diff --git a/cmd/build.go b/cmd/build.go index 0965da1..b562af8 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -2,8 +2,6 @@ package cmd import ( "github.com/qiniu/goc/v2/pkg/build" - "github.com/qiniu/goc/v2/pkg/config" - "github.com/qiniu/goc/v2/pkg/flag" "github.com/spf13/cobra" ) @@ -14,15 +12,29 @@ var buildCmd = &cobra.Command{ DisableFlagParsing: true, // build 命令需要用原生 go 的方式处理 flags } +var ( + gocmode string + gochost string +) + func init() { - buildCmd.Flags().StringVarP(&config.GocConfig.Mode, "gocmode", "", "count", "coverage mode: set, count, atomic, watch") - buildCmd.Flags().StringVarP(&config.GocConfig.Host, "gochost", "", "127.0.0.1:7777", "specify the host of the goc sever") + buildCmd.Flags().StringVarP(&gocmode, "gocmode", "", "count", "coverage mode: set, count, atomic, watch") + buildCmd.Flags().StringVarP(&gochost, "gochost", "", "127.0.0.1:7777", "specify the host of the goc sever") rootCmd.AddCommand(buildCmd) } func buildAction(cmd *cobra.Command, args []string) { - // 1. 解析 goc 命令行和 go 命令行 - remainedArgs := flag.BuildCmdArgsParse(cmd, args, flag.GO_BUILD) - b := build.NewBuild(remainedArgs) + + sets := build.CustomParseCmdAndArgs(cmd, args) + + b := build.NewBuild( + build.WithHost(gochost), + build.WithMode(gocmode), + build.WithFlagSets(sets), + build.WithArgs(args), + build.WithBuild(), + build.WithDebug(globalDebug), + ) b.Build() + } diff --git a/cmd/install.go b/cmd/install.go index 7396080..0fcb6a2 100644 --- a/cmd/install.go +++ b/cmd/install.go @@ -2,8 +2,6 @@ package cmd import ( "github.com/qiniu/goc/v2/pkg/build" - "github.com/qiniu/goc/v2/pkg/config" - "github.com/qiniu/goc/v2/pkg/flag" "github.com/spf13/cobra" ) @@ -15,14 +13,23 @@ var installCmd = &cobra.Command{ } func init() { - installCmd.Flags().StringVarP(&config.GocConfig.Mode, "gocmode", "", "count", "coverage mode: set, count, atomic, watch") - installCmd.Flags().StringVarP(&config.GocConfig.Host, "gochost", "", "127.0.0.1:7777", "specify the host of the goc sever") + installCmd.Flags().StringVarP(&gocmode, "gocmode", "", "count", "coverage mode: set, count, atomic, watch") + installCmd.Flags().StringVarP(&gochost, "gochost", "", "127.0.0.1:7777", "specify the host of the goc sever") rootCmd.AddCommand(installCmd) } func installAction(cmd *cobra.Command, args []string) { - // 1. 解析 goc 命令行和 go 命令行 - remainedArgs := flag.BuildCmdArgsParse(cmd, args, flag.GO_INSTALL) - b := build.NewInstall(remainedArgs) + + sets := build.CustomParseCmdAndArgs(cmd, args) + + b := build.NewInstall( + build.WithHost(gochost), + build.WithMode(gocmode), + build.WithFlagSets(sets), + build.WithArgs(args), + build.WithInstall(), + build.WithDebug(globalDebug), + ) b.Install() + } diff --git a/cmd/list.go b/cmd/list.go index 1fb5bec..fff1927 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -2,7 +2,6 @@ package cmd import ( "github.com/qiniu/goc/v2/pkg/client" - "github.com/qiniu/goc/v2/pkg/config" "github.com/spf13/cobra" ) @@ -17,14 +16,17 @@ goc list [flags] Run: list, } -var listWide bool +var ( + listHost string + listWide bool +) func init() { - listCmd.Flags().StringVar(&config.GocConfig.Host, "host", "127.0.0.1:7777", "specify the host of the goc server") + listCmd.Flags().StringVar(&listHost, "host", "127.0.0.1:7777", "specify the host of the goc server") listCmd.Flags().BoolVar(&listWide, "wide", false, "list all services with more information (such as pid)") rootCmd.AddCommand(listCmd) } func list(cmd *cobra.Command, args []string) { - client.NewWorker("http://" + config.GocConfig.Host).ListAgents(listWide) + client.NewWorker("http://" + listHost).ListAgents(listWide) } diff --git a/cmd/profile.go b/cmd/profile.go index 9a6626b..7cb3525 100644 --- a/cmd/profile.go +++ b/cmd/profile.go @@ -2,7 +2,6 @@ package cmd import ( "github.com/qiniu/goc/v2/pkg/client" - "github.com/qiniu/goc/v2/pkg/config" "github.com/spf13/cobra" ) @@ -19,14 +18,17 @@ goc profile --host=http://192.168.1.1:8080 --output=./coverage.cov Run: profile, } -var output string // --output flag +var ( + profileHost string + profileoutput string // --output flag +) func init() { - profileCmd.Flags().StringVar(&config.GocConfig.Host, "host", "127.0.0.1:7777", "specify the host of the goc server") - profileCmd.Flags().StringVarP(&output, "output", "o", "", "download cover profile") + profileCmd.Flags().StringVar(&profileHost, "host", "127.0.0.1:7777", "specify the host of the goc server") + profileCmd.Flags().StringVarP(&profileoutput, "output", "o", "", "download cover profile") rootCmd.AddCommand(profileCmd) } func profile(cmd *cobra.Command, args []string) { - client.NewWorker("http://" + config.GocConfig.Host).Profile(output) + client.NewWorker("http://" + profileHost).Profile(profileoutput) } diff --git a/cmd/root.go b/cmd/root.go index 6e01b28..4c48b78 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -14,7 +14,6 @@ package cmd import ( - "github.com/qiniu/goc/v2/pkg/config" "github.com/qiniu/goc/v2/pkg/log" "github.com/spf13/cobra" ) @@ -30,7 +29,7 @@ Find more information at: PersistentPreRun: func(cmd *cobra.Command, args []string) { log.DisplayGoc() // init logger - log.NewLogger() + log.NewLogger(globalDebug) }, PersistentPostRun: func(cmd *cobra.Command, args []string) { @@ -38,8 +37,10 @@ Find more information at: }, } +var globalDebug bool + func init() { - rootCmd.PersistentFlags().BoolVar(&config.GocConfig.Debug, "gocdebug", false, "run goc in debug mode") + rootCmd.PersistentFlags().BoolVar(&globalDebug, "gocdebug", false, "run goc in debug mode") } // Execute the goc tool diff --git a/cmd/server.go b/cmd/server.go index 4b74218..05a7784 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -1,7 +1,6 @@ package cmd import ( - "github.com/qiniu/goc/v2/pkg/config" "github.com/qiniu/goc/v2/pkg/server" "github.com/spf13/cobra" ) @@ -15,13 +14,15 @@ var serverCmd = &cobra.Command{ Run: serve, } +var ( + serverHost string +) + func init() { - // serverCmd.Flags().IntVarP(&config.GocConfig.Port, "port", "", 7777, "listen port to start a coverage host center") - // serverCmd.Flags().StringVarP(&config.GocConfig.StorePath, "storepath", "", "goc.store", "the file to save all goc server information") - serverCmd.Flags().StringVarP(&config.GocConfig.Host, "host", "", "127.0.0.1:7777", "specify the host of the goc server") + serverCmd.Flags().StringVarP(&serverHost, "host", "", "127.0.0.1:7777", "specify the host of the goc server") rootCmd.AddCommand(serverCmd) } func serve(cmd *cobra.Command, args []string) { - server.RunGocServerUntilExit(config.GocConfig.Host) + server.RunGocServerUntilExit(serverHost) } diff --git a/cmd/watch.go b/cmd/watch.go index 427e88b..ee7f616 100644 --- a/cmd/watch.go +++ b/cmd/watch.go @@ -1,7 +1,6 @@ package cmd import ( - "github.com/qiniu/goc/v2/pkg/config" cli "github.com/qiniu/goc/v2/pkg/watch" "github.com/spf13/cobra" ) @@ -15,11 +14,15 @@ var watchCmd = &cobra.Command{ Run: watch, } +var ( + watchHost string +) + func init() { - watchCmd.Flags().StringVarP(&config.GocConfig.Host, "host", "", "127.0.0.1:7777", "specify the host of the goc server") + watchCmd.Flags().StringVarP(&watchHost, "host", "", "127.0.0.1:7777", "specify the host of the goc server") rootCmd.AddCommand(watchCmd) } func watch(cmd *cobra.Command, args []string) { - cli.Watch() + cli.Watch(watchHost) } diff --git a/pkg/cover/agent.tpl b/pkg/build/agent.tpl similarity index 100% rename from pkg/cover/agent.tpl rename to pkg/build/agent.tpl diff --git a/pkg/cover/agentwatch.tpl b/pkg/build/agentwatch.tpl similarity index 100% rename from pkg/cover/agentwatch.tpl rename to pkg/build/agentwatch.tpl diff --git a/pkg/build/build.go b/pkg/build/build.go index e1202e7..e5c9ac1 100644 --- a/pkg/build/build.go +++ b/pkg/build/build.go @@ -3,25 +3,52 @@ package build import ( "os" "os/exec" + "strings" - "github.com/qiniu/goc/v2/pkg/config" - "github.com/qiniu/goc/v2/pkg/cover" - "github.com/qiniu/goc/v2/pkg/flag" "github.com/qiniu/goc/v2/pkg/log" + "github.com/spf13/pflag" ) // Build struct a build -// most configurations are stored in global variables: config.GocConfig & config.GoConfig type Build struct { + Args []string // all goc + go command line args + flags + FlagSets *pflag.FlagSet + BuildType int + + Debug bool + Host string + Mode string // cover mode + + GOPATH string + GOBIN string + CurWd string + TmpWd string + CurModProjectDir string + TmpModProjectDir string + + Goflags []string // go command line flags + GoArgs []string // go command line args + Packages []string // go command line [packages] + + ImportPath string // the whole import path of the project + Pkgs map[string]*Package + GlobalCoverVarImportPath string + GlobalCoverVarImportPathDir string } // NewBuild creates a Build struct // -func NewBuild(args []string) *Build { +func NewBuild(opts ...GocOption) *Build { b := &Build{} + for _, opt := range opts { + opt(b) + } + + // 1. 解析 goc 命令行和 go 命令行 + b.buildCmdArgsParse() // 2. 解析 go 包位置 - flag.GetPackagesDir(args) + b.getPackagesDir() // 3. 读取工程元信息:go.mod, pkgs list ... b.readProjectMetaInfo() // 4. 展示元信息 @@ -41,16 +68,19 @@ func (b *Build) Build() { defer b.clean() log.Donef("project copied to temporary directory") - // 2. inject cover vars - cover.Inject() - // 3. build in the temp project + + // 2. update go.mod file if needed + b.updateGoModFile() + // 3. inject cover vars + b.Inject() + // 4. build in the temp project b.doBuildInTemp() } func (b *Build) doBuildInTemp() { log.StartWait("building the injected project") - goflags := config.GocConfig.Goflags + goflags := b.Goflags // 检查用户是否自定义了 -o oSet := false for _, flag := range goflags { @@ -61,10 +91,10 @@ func (b *Build) doBuildInTemp() { // 如果没被设置就加一个至原命令执行的目录 if !oSet { - goflags = append(goflags, "-o", config.GocConfig.CurWd) + goflags = append(goflags, "-o", b.CurWd) } - pacakges := config.GocConfig.Packages + pacakges := b.Packages goflags = append(goflags, pacakges...) @@ -72,11 +102,11 @@ func (b *Build) doBuildInTemp() { args = append(args, goflags...) // go 命令行由 go build [-o output] [build flags] [packages] 组成 cmd := exec.Command("go", args...) - cmd.Dir = config.GocConfig.TmpWd + cmd.Dir = b.TmpWd cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - log.Infof("go build cmd is: %v, in path [%v]", cmd.Args, cmd.Dir) + log.Infof("go build cmd is: %v, in path [%v]", nicePrintArgs(cmd.Args), cmd.Dir) if err := cmd.Start(); err != nil { log.Fatalf("fail to execute go build: %v", err) } @@ -88,3 +118,21 @@ func (b *Build) doBuildInTemp() { log.StopWait() log.Donef("go build done") } + +// nicePrintArgs 优化 args 打印内容 +// +// 假如:go build -ldflags "-X my/package/config.Version=1.0.0" -o /home/lyy/gitdown/gin-test/cmd . +// +// 实际输出会变为:go build -ldflags -X my/package/config.Version=1.0.0 -o /home/lyy/gitdown/gin-test/cmd . +func nicePrintArgs(args []string) []string { + output := make([]string, 0) + for _, arg := range args { + if strings.Contains(arg, " ") { + output = append(output, "\""+arg+"\"") + } else { + output = append(output, arg) + } + } + + return output +} diff --git a/pkg/build/build_flags.go b/pkg/build/build_flags.go new file mode 100644 index 0000000..2db94a7 --- /dev/null +++ b/pkg/build/build_flags.go @@ -0,0 +1,425 @@ +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 "" +} diff --git a/pkg/build/config.go b/pkg/build/config.go new file mode 100644 index 0000000..7e9d164 --- /dev/null +++ b/pkg/build/config.go @@ -0,0 +1,49 @@ +package build + +import ( + "github.com/spf13/pflag" +) + +type GocOption func(*Build) + +func WithHost(host string) GocOption { + return func(b *Build) { + b.Host = host + } +} + +func WithMode(mode string) GocOption { + return func(b *Build) { + b.Mode = mode + } +} + +func WithArgs(args []string) GocOption { + return func(b *Build) { + b.Args = args + } +} + +func WithFlagSets(sets *pflag.FlagSet) GocOption { + return func(b *Build) { + b.FlagSets = sets + } +} + +func WithBuild() GocOption { + return func(b *Build) { + b.BuildType = 0 + } +} + +func WithInstall() GocOption { + return func(b *Build) { + b.BuildType = 1 + } +} + +func WithDebug(enable bool) GocOption { + return func(b *Build) { + b.Debug = enable + } +} diff --git a/pkg/config/config.go b/pkg/build/cover.go similarity index 66% rename from pkg/config/config.go rename to pkg/build/cover.go index 581698e..21050d8 100644 --- a/pkg/config/config.go +++ b/pkg/build/cover.go @@ -1,69 +1,48 @@ -package config +package build -import "time" +import ( + "crypto/sha256" + "fmt" + "path" + "time" +) -type gocConfig struct { - Debug bool - Host string - Mode string // cover mode +// declareCoverVars attaches the required cover variables names +// to the files, to be used when annotating the files. +func declareCoverVars(p *Package) map[string]*FileVar { + coverVars := make(map[string]*FileVar) + coverIndex := 0 + // We create the cover counters as new top-level variables in the package. + // We need to avoid collisions with user variables (GoCover_0 is unlikely but still) + // and more importantly with dot imports of other covered packages, + // so we append 12 hex digits from the SHA-256 of the import path. + // The point is only to avoid accidents, not to defeat users determined to + // break things. + sum := sha256.Sum256([]byte(p.ImportPath)) + h := fmt.Sprintf("%x", sum[:6]) + for _, file := range p.GoFiles { + // These names appear in the cmd/cover HTML interface. + var longFile = path.Join(p.ImportPath, file) + coverVars[file] = &FileVar{ + File: longFile, + Var: fmt.Sprintf("GoCover_%d_%x", coverIndex, h), + } + coverIndex++ + } - GOPATH string - GOBIN string - CurWd string - TmpWd string - CurModProjectDir string - TmpModProjectDir string + for _, file := range p.CgoFiles { + // These names appear in the cmd/cover HTML interface. + var longFile = path.Join(p.ImportPath, file) + coverVars[file] = &FileVar{ + File: longFile, + Var: fmt.Sprintf("GoCover_%d_%x", coverIndex, h), + } + coverIndex++ + } - Goflags []string // command line flags - Packages []string // command line [packages] - - ImportPath string // the whole import path of the project - Pkgs map[string]*Package - GlobalCoverVarImportPath string - GlobalCoverVarImportPathDir string + return coverVars } -// GocConfig 全局变量,存放 goc 的各种元属性 -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 - // PackageCover holds all the generate coverage variables of a package type PackageCover struct { Package *Package diff --git a/pkg/build/goenv.go b/pkg/build/goenv.go index 7dc3b87..6926226 100644 --- a/pkg/build/goenv.go +++ b/pkg/build/goenv.go @@ -10,17 +10,16 @@ import ( "path/filepath" "strings" - "github.com/qiniu/goc/v2/pkg/config" "github.com/qiniu/goc/v2/pkg/log" ) // readProjectMetaInfo reads all meta informations of the corresponding project func (b *Build) readProjectMetaInfo() { // get gopath & gobin - config.GocConfig.GOPATH = b.readGOPATH() - config.GocConfig.GOBIN = b.readGOBIN() + b.GOPATH = b.readGOPATH() + b.GOBIN = b.readGOBIN() // 获取 [packages] 及其依赖的 package list - pkgs := b.listPackages(config.GocConfig.CurWd) + pkgs := b.listPackages(b.CurWd) // get mod info for _, pkg := range pkgs { @@ -29,34 +28,34 @@ func (b *Build) readProjectMetaInfo() { log.Fatalf("Go module is not enabled, please set GO111MODULE=auto or on") } // 工程根目录 - config.GocConfig.CurModProjectDir = pkg.Module.Dir - config.GocConfig.ImportPath = pkg.Module.Path + b.CurModProjectDir = pkg.Module.Dir + b.ImportPath = pkg.Module.Path break } // 如果当前目录不是工程根目录,那再次 go list 一次,获取整个工程的包信息 - if config.GocConfig.CurWd != config.GocConfig.CurModProjectDir { - config.GocConfig.Pkgs = b.listPackages(config.GocConfig.CurModProjectDir) + if b.CurWd != b.CurModProjectDir { + b.Pkgs = b.listPackages(b.CurModProjectDir) } else { - config.GocConfig.Pkgs = pkgs + b.Pkgs = pkgs } // get tmp folder name - config.GocConfig.TmpModProjectDir = filepath.Join(os.TempDir(), TmpFolderName(config.GocConfig.CurModProjectDir)) + b.TmpModProjectDir = filepath.Join(os.TempDir(), TmpFolderName(b.CurModProjectDir)) // get working dir in the corresponding tmp dir - config.GocConfig.TmpWd = filepath.Join(config.GocConfig.TmpModProjectDir, config.GocConfig.CurWd[len(config.GocConfig.CurModProjectDir):]) + b.TmpWd = filepath.Join(b.TmpModProjectDir, b.CurWd[len(b.CurModProjectDir):]) // get GlobalCoverVarImportPath - config.GocConfig.GlobalCoverVarImportPath = path.Join(config.GocConfig.ImportPath, TmpFolderName(config.GocConfig.CurModProjectDir)) + b.GlobalCoverVarImportPath = path.Join(b.ImportPath, TmpFolderName(b.CurModProjectDir)) log.Donef("project meta information parsed") } // displayProjectMetaInfo prints basic infomation of this project to stdout func (b *Build) displayProjectMetaInfo() { - log.Infof("GOPATH: %v", config.GocConfig.GOPATH) - log.Infof("GOBIN: %v", config.GocConfig.GOBIN) - log.Infof("Project Directory: %v", config.GocConfig.CurModProjectDir) - log.Infof("Temporary Project Directory: %v", config.GocConfig.TmpModProjectDir) + log.Infof("GOPATH: %v", b.GOPATH) + log.Infof("GOBIN: %v", b.GOBIN) + log.Infof("Project Directory: %v", b.CurModProjectDir) + log.Infof("Temporary Project Directory: %v", b.TmpModProjectDir) log.Infof("") } @@ -79,7 +78,7 @@ func (b *Build) readGOBIN() string { } // listPackages list all packages under specific via go list command. -func (b *Build) listPackages(dir string) map[string]*config.Package { +func (b *Build) listPackages(dir string) map[string]*Package { cmd := exec.Command("go", "list", "-json", "./...") cmd.Dir = dir @@ -95,10 +94,10 @@ func (b *Build) listPackages(dir string) map[string]*config.Package { } dec := json.NewDecoder(bytes.NewBuffer(out)) - pkgs := make(map[string]*config.Package, 0) + pkgs := make(map[string]*Package) for { - var pkg config.Package + var pkg Package if err := dec.Decode(&pkg); err != nil { if err == io.EOF { break diff --git a/pkg/cover/inject.go b/pkg/build/inject.go similarity index 74% rename from pkg/cover/inject.go rename to pkg/build/inject.go index 2abc9e5..73089c0 100644 --- a/pkg/cover/inject.go +++ b/pkg/build/inject.go @@ -1,4 +1,4 @@ -package cover +package build import ( "fmt" @@ -6,30 +6,28 @@ import ( "path" "path/filepath" - "github.com/qiniu/goc/v2/pkg/config" - "github.com/qiniu/goc/v2/pkg/cover/internal/tool" - "github.com/qiniu/goc/v2/pkg/cover/internal/websocket" + "github.com/qiniu/goc/v2/pkg/build/internal/tool" + "github.com/qiniu/goc/v2/pkg/build/internal/websocket" "github.com/qiniu/goc/v2/pkg/log" ) // Inject injects cover variables for all the .go files in the target directory -func Inject() { +func (b *Build) Inject() { log.StartWait("injecting cover variables") - var seen = make(map[string]*config.PackageCover) + var seen = make(map[string]*PackageCover) // 所有插桩变量定义声明 allDecl := "" - pkgs := config.GocConfig.Pkgs + pkgs := b.Pkgs for _, pkg := range pkgs { if pkg.Name == "main" { - log.Infof("handle main package: %v", pkg.ImportPath) // 该 main 二进制所关联的所有插桩变量的元信息 // 每个 main 之间是不相关的,需要重新定义 - allMainCovers := make([]*config.PackageCover, 0) + allMainCovers := make([]*PackageCover, 0) // 注入 main package - mainCover, mainDecl := addCounters(pkg) + mainCover, mainDecl := b.addCounters(pkg) // 收集插桩变量的定义和元信息 allDecl += mainDecl allMainCovers = append(allMainCovers, mainCover) @@ -43,7 +41,7 @@ func Inject() { // 依赖需要忽略 Go 标准库和 go.mod 引入的第三方 if depPkg, ok := pkgs[dep]; ok { // 注入依赖的 package - packageCover, depDecl := addCounters(depPkg) + packageCover, depDecl := b.addCounters(depPkg) // 收集插桩变量的定义和元信息 allDecl += depDecl allMainCovers = append(allMainCovers, packageCover) @@ -52,20 +50,25 @@ func Inject() { } } // 为每个 main 包注入 websocket handler - injectGocAgent(getPkgTmpDir(pkg.Dir), allMainCovers) + b.injectGocAgent(b.getPkgTmpDir(pkg.Dir), allMainCovers) + if b.Mode == "watch" { + log.Donef("inject main package [%v] with rpcagent and watchagent", pkg.ImportPath) + } else { + log.Donef("inject main package [%v] with rpcagent", pkg.ImportPath) + } } } // 在工程根目录注入所有插桩变量的声明+定义 - injectGlobalCoverVarFile(allDecl) + b.injectGlobalCoverVarFile(allDecl) // 添加自定义 websocket 依赖 // 用户代码可能有 gorrila/websocket 的依赖,为避免依赖冲突,以及可能的 replace/vendor, // 这里直接注入一份完整的 gorrila/websocket 实现 - websocket.AddCustomWebsocketDep() + websocket.AddCustomWebsocketDep(b.GlobalCoverVarImportPathDir) log.Donef("websocket library injected") log.StopWait() - log.Donef("cover variables injected") + log.Donef("global cover variables injected") } // addCounters is different from official go tool cover @@ -75,18 +78,18 @@ func Inject() { // 2. no declarartions for these covervars // // 3. return the declarations as string -func addCounters(pkg *config.Package) (*config.PackageCover, string) { - mode := config.GocConfig.Mode - gobalCoverVarImportPath := config.GocConfig.GlobalCoverVarImportPath +func (b *Build) addCounters(pkg *Package) (*PackageCover, string) { + mode := b.Mode + gobalCoverVarImportPath := b.GlobalCoverVarImportPath coverVarMap := declareCoverVars(pkg) decl := "" for file, coverVar := range coverVarMap { - decl += "\n" + tool.Annotate(filepath.Join(getPkgTmpDir(pkg.Dir), file), mode, coverVar.Var, coverVar.File, gobalCoverVarImportPath) + "\n" + decl += "\n" + tool.Annotate(filepath.Join(b.getPkgTmpDir(pkg.Dir), file), mode, coverVar.Var, coverVar.File, gobalCoverVarImportPath) + "\n" } - return &config.PackageCover{ + return &PackageCover{ Package: pkg, Vars: coverVarMap, }, decl @@ -99,13 +102,13 @@ func addCounters(pkg *config.Package) (*config.PackageCover, string) { // // 在原工程目录已经做了一次 go list -json,在临时目录没有必要再做一遍,直接转换一下就能得到 // 临时目录中的 pkg.Dir。 -func getPkgTmpDir(pkgDir string) string { - relDir, err := filepath.Rel(config.GocConfig.CurModProjectDir, pkgDir) +func (b *Build) getPkgTmpDir(pkgDir string) string { + relDir, err := filepath.Rel(b.CurModProjectDir, pkgDir) if err != nil { log.Fatalf("go json -list meta info wrong: %v", err) } - return filepath.Join(config.GocConfig.TmpModProjectDir, relDir) + return filepath.Join(b.TmpModProjectDir, relDir) } // injectGocAgent inject handlers like following @@ -121,7 +124,7 @@ func getPkgTmpDir(pkgDir string) string { // // 11111_22222_bridge.go 仅仅用于引用 11111_22222_package, where package contains ws agent main logic. // 使用 bridge.go 文件是为了避免插桩逻辑中的变量名污染 main 包 -func injectGocAgent(where string, covers []*config.PackageCover) { +func (b *Build) injectGocAgent(where string, covers []*PackageCover) { injectPkgName := "goc-cover-agent-apis-auto-generated-11111-22222-package" injectBridgeName := "goc-cover-agent-apis-auto-generated-11111-22222-bridge.go" wherePkg := filepath.Join(where, injectPkgName) @@ -159,22 +162,22 @@ func injectGocAgent(where string, covers []*config.PackageCover) { defer f2.Close() var _coverMode string - if config.GocConfig.Mode == "watch" { + if b.Mode == "watch" { _coverMode = "cover" } else { - _coverMode = config.GocConfig.Mode + _coverMode = b.Mode } tmplData := struct { - Covers []*config.PackageCover + Covers []*PackageCover GlobalCoverVarImportPath string Package string Host string Mode string }{ Covers: covers, - GlobalCoverVarImportPath: config.GocConfig.GlobalCoverVarImportPath, + GlobalCoverVarImportPath: b.GlobalCoverVarImportPath, Package: injectPkgName, - Host: config.GocConfig.Host, + Host: b.Host, Mode: _coverMode, } @@ -183,7 +186,7 @@ func injectGocAgent(where string, covers []*config.PackageCover) { } // 写入 watch - if config.GocConfig.Mode != "watch" { + if b.Mode != "watch" { return } f, err := os.Create(filepath.Join(wherePkg, "watchagent.go")) @@ -196,9 +199,9 @@ func injectGocAgent(where string, covers []*config.PackageCover) { Host string GlobalCoverVarImportPath string }{ - Random: filepath.Base(config.GocConfig.TmpModProjectDir), - Host: config.GocConfig.Host, - GlobalCoverVarImportPath: config.GocConfig.GlobalCoverVarImportPath, + Random: filepath.Base(b.TmpModProjectDir), + Host: b.Host, + GlobalCoverVarImportPath: b.GlobalCoverVarImportPath, } if err := coverWatchTmpl.Execute(f, tmplwatchData); err != nil { @@ -207,10 +210,10 @@ func injectGocAgent(where string, covers []*config.PackageCover) { } // injectGlobalCoverVarFile 写入所有插桩变量的全局定义至一个单独的文件 -func injectGlobalCoverVarFile(decl string) { - globalCoverVarPackage := path.Base(config.GocConfig.GlobalCoverVarImportPath) - globalCoverDef := filepath.Join(config.GocConfig.TmpModProjectDir, globalCoverVarPackage) - config.GocConfig.GlobalCoverVarImportPathDir = globalCoverDef +func (b *Build) injectGlobalCoverVarFile(decl string) { + globalCoverVarPackage := path.Base(b.GlobalCoverVarImportPath) + globalCoverDef := filepath.Join(b.TmpModProjectDir, globalCoverVarPackage) + b.GlobalCoverVarImportPathDir = globalCoverDef err := os.MkdirAll(globalCoverDef, os.ModePerm) if err != nil { @@ -225,7 +228,7 @@ func injectGlobalCoverVarFile(decl string) { packageName := "package coverdef\n\n" - random := filepath.Base(config.GocConfig.TmpModProjectDir) + random := filepath.Base(b.TmpModProjectDir) varWatchDef := fmt.Sprintf(` var WatchChannel_%v = make(chan *blockInfo, 1024) diff --git a/pkg/build/install.go b/pkg/build/install.go index a2484f2..1bb7a3d 100644 --- a/pkg/build/install.go +++ b/pkg/build/install.go @@ -4,13 +4,11 @@ import ( "os" "os/exec" - "github.com/qiniu/goc/v2/pkg/config" - "github.com/qiniu/goc/v2/pkg/cover" "github.com/qiniu/goc/v2/pkg/log" ) -func NewInstall(args []string) *Build { - return NewBuild(args) +func NewInstall(opts ...GocOption) *Build { + return NewBuild(opts...) } // Install starts go install @@ -24,18 +22,21 @@ func (b *Build) Install() { defer b.clean() log.Donef("project copied to temporary directory") - // 2. inject cover vars - cover.Inject() - // 3. install in the temp project + + // 2. update go.mod file if needed + b.updateGoModFile() + // 3. inject cover vars + b.Inject() + // 4. install in the temp project b.doInstallInTemp() } func (b *Build) doInstallInTemp() { log.StartWait("installing the injected project") - goflags := config.GocConfig.Goflags + goflags := b.Goflags - pacakges := config.GocConfig.Packages + pacakges := b.Packages goflags = append(goflags, pacakges...) @@ -43,7 +44,7 @@ func (b *Build) doInstallInTemp() { args = append(args, goflags...) // go 命令行由 go install [build flags] [packages] 组成 cmd := exec.Command("go", args...) - cmd.Dir = config.GocConfig.TmpWd + cmd.Dir = b.TmpWd cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/pkg/cover/internal/tool/cover.go b/pkg/build/internal/tool/cover.go similarity index 100% rename from pkg/cover/internal/tool/cover.go rename to pkg/build/internal/tool/cover.go diff --git a/pkg/cover/internal/tool/edit.go b/pkg/build/internal/tool/edit.go similarity index 100% rename from pkg/cover/internal/tool/edit.go rename to pkg/build/internal/tool/edit.go diff --git a/pkg/cover/internal/websocket/websocket.tar b/pkg/build/internal/websocket/websocket.tar similarity index 100% rename from pkg/cover/internal/websocket/websocket.tar rename to pkg/build/internal/websocket/websocket.tar diff --git a/pkg/cover/internal/websocket/wsdep.go b/pkg/build/internal/websocket/wsdep.go similarity index 92% rename from pkg/cover/internal/websocket/wsdep.go rename to pkg/build/internal/websocket/wsdep.go index 7176a39..6c3b898 100644 --- a/pkg/cover/internal/websocket/wsdep.go +++ b/pkg/build/internal/websocket/wsdep.go @@ -8,7 +8,6 @@ import ( "os" "path/filepath" - "github.com/qiniu/goc/v2/pkg/config" "github.com/qiniu/goc/v2/pkg/log" ) @@ -19,7 +18,7 @@ var depTarFile embed.FS // // 从 embed 文件系统中解压 websocket.tar 文件,并依次写入临时工程中,作为一个单独的包存在。 // gorrila/websocket 是一个无第三方依赖的库,因此其位置可以随处移动,而不影响自身的编译。 -func AddCustomWebsocketDep() { +func AddCustomWebsocketDep(customWebsocketPath string) { data, err := depTarFile.ReadFile("websocket.tar") if err != nil { log.Fatalf("cannot find the websocket.tar in the embed file: %v", err) @@ -36,7 +35,6 @@ func AddCustomWebsocketDep() { log.Fatalf("cannot untar the websocket.tar: %v", err) } - customWebsocketPath := config.GocConfig.GlobalCoverVarImportPathDir fpath := filepath.Join(customWebsocketPath, hdr.Name) if hdr.FileInfo().IsDir() { // 处理目录 diff --git a/pkg/cover/template.go b/pkg/build/template.go similarity index 97% rename from pkg/cover/template.go rename to pkg/build/template.go index 7e39a7c..ef392ea 100644 --- a/pkg/cover/template.go +++ b/pkg/build/template.go @@ -1,4 +1,4 @@ -package cover +package build import ( _ "embed" diff --git a/pkg/build/tmpfolder.go b/pkg/build/tmpfolder.go index b847ef1..bbffbc5 100644 --- a/pkg/build/tmpfolder.go +++ b/pkg/build/tmpfolder.go @@ -8,7 +8,6 @@ import ( "path/filepath" "strings" - "github.com/qiniu/goc/v2/pkg/config" "github.com/qiniu/goc/v2/pkg/log" "github.com/tongjingran/copy" "golang.org/x/mod/modfile" @@ -18,8 +17,8 @@ import ( // // It will ignore .git and irregular files, only copy source(text) files func (b *Build) copyProjectToTmp() { - curProject := config.GocConfig.CurModProjectDir - tmpProject := config.GocConfig.TmpModProjectDir + curProject := b.CurModProjectDir + tmpProject := b.TmpModProjectDir if _, err := os.Stat(tmpProject); !os.IsNotExist(err) { log.Infof("find previous temporary directory, delete") @@ -67,8 +66,8 @@ func skipCopy(src string, info os.FileInfo) (bool, error) { // clean clears the temporary project func (b *Build) clean() { - if config.GocConfig.Debug != true { - if err := os.RemoveAll(config.GocConfig.TmpModProjectDir); err != nil { + if !b.Debug { + if err := os.RemoveAll(b.TmpModProjectDir); err != nil { log.Fatalf("fail to delete the temporary project: %v", err) } log.Donef("delete the temporary project") @@ -90,7 +89,7 @@ func (b *Build) clean() { // after the project is copied to temporary directory, it should be rewritten as // 'replace github.com/qiniu/bar => /path/to/aa/bb/home/foo/bar' func (b *Build) updateGoModFile() (updateFlag bool, newModFile []byte) { - tempModfile := filepath.Join(config.GocConfig.TmpModProjectDir, "go.mod") + tempModfile := filepath.Join(b.TmpModProjectDir, "go.mod") buf, err := ioutil.ReadFile(tempModfile) if err != nil { log.Fatalf("cannot find go.mod file in temporary directory: %v", err) @@ -111,7 +110,7 @@ func (b *Build) updateGoModFile() (updateFlag bool, newModFile []byte) { // absolute path no need to rewrite if newVersion == "" && !filepath.IsAbs(newPath) { var absPath string - fullPath := filepath.Join(config.GocConfig.CurModProjectDir, newPath) + fullPath := filepath.Join(b.CurModProjectDir, newPath) absPath, _ = filepath.Abs(fullPath) // DropReplace & AddReplace will not return error // so no need to check the error @@ -126,5 +125,13 @@ func (b *Build) updateGoModFile() (updateFlag bool, newModFile []byte) { // return Format(f.Syntax), nil // } newModFile, _ = oriGoModFile.Format() + + if updateFlag { + log.Infof("go.mod needs rewrite") + err := os.WriteFile(tempModfile, newModFile, os.ModePerm) + if err != nil { + log.Fatalf("fail to update go.mod: %v", err) + } + } return } diff --git a/pkg/cover/cover.go b/pkg/cover/cover.go deleted file mode 100644 index 1c4d149..0000000 --- a/pkg/cover/cover.go +++ /dev/null @@ -1,45 +0,0 @@ -package cover - -import ( - "crypto/sha256" - "fmt" - "path" - - "github.com/qiniu/goc/v2/pkg/config" -) - -// declareCoverVars attaches the required cover variables names -// to the files, to be used when annotating the files. -func declareCoverVars(p *config.Package) map[string]*config.FileVar { - coverVars := make(map[string]*config.FileVar) - coverIndex := 0 - // We create the cover counters as new top-level variables in the package. - // We need to avoid collisions with user variables (GoCover_0 is unlikely but still) - // and more importantly with dot imports of other covered packages, - // so we append 12 hex digits from the SHA-256 of the import path. - // The point is only to avoid accidents, not to defeat users determined to - // break things. - sum := sha256.Sum256([]byte(p.ImportPath)) - h := fmt.Sprintf("%x", sum[:6]) - for _, file := range p.GoFiles { - // These names appear in the cmd/cover HTML interface. - var longFile = path.Join(p.ImportPath, file) - coverVars[file] = &config.FileVar{ - File: longFile, - Var: fmt.Sprintf("GoCover_%d_%x", coverIndex, h), - } - coverIndex++ - } - - for _, file := range p.CgoFiles { - // These names appear in the cmd/cover HTML interface. - var longFile = path.Join(p.ImportPath, file) - coverVars[file] = &config.FileVar{ - File: longFile, - Var: fmt.Sprintf("GoCover_%d_%x", coverIndex, h), - } - coverIndex++ - } - - return coverVars -} diff --git a/pkg/flag/build_flags.go b/pkg/flag/build_flags.go deleted file mode 100644 index 75857bd..0000000 --- a/pkg/flag/build_flags.go +++ /dev/null @@ -1,146 +0,0 @@ -package flag - -import ( - "flag" - "os" - "path/filepath" - - "github.com/qiniu/goc/v2/pkg/config" - "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 -) - -// BuildCmdArgsParse parse both go flags and goc flags, it rewrite go flags if -// necessary, and returns all non-flag arguments. -// -// 吞下 [packages] 之前所有的 flags. -func BuildCmdArgsParse(cmd *cobra.Command, args []string, cmdType int) []string { - // 首先解析 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) - - // 重写 help - helpFlag := allFlagSets.Lookup("help") - - if helpFlag.Changed { - if cmdType == GO_BUILD { - printHelp(buildUsage, cmd) - } else if cmdType == GO_INSTALL { - printHelp(installUsage, cmd) - } - - os.Exit(0) - } - // 删除 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, 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) - } - config.GocConfig.CurWd = curWd - 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()) - } - }) - - config.GocConfig.Goflags = flags - - return goFlagSets.Args() -} - -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 -} diff --git a/pkg/flag/flags.go b/pkg/flag/flags.go deleted file mode 100644 index 6cbfb1a..0000000 --- a/pkg/flag/flags.go +++ /dev/null @@ -1,40 +0,0 @@ -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 deleted file mode 100644 index 78fe230..0000000 --- a/pkg/flag/help.go +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index 6c7ca27..0000000 --- a/pkg/flag/packages.go +++ /dev/null @@ -1,193 +0,0 @@ -package flag - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/qiniu/goc/v2/pkg/config" - "github.com/qiniu/goc/v2/pkg/log" -) - -// GetPackagesDir parse [pacakges] part of args, it will fatal if error encountered -// -// 函数获取 1: [packages] 所在的目录位置,供后续插桩使用。 -// -// 函数获取 2: 如果参数是 *.go,第一个 .go 文件的文件名。go build 中,二进制名字既可能是目录名也可能是文件名,和参数类型有关。 -// -// 如果 [packages] 非法(即不符合 go 原生的定义),则返回对应错误 -// 这里只考虑 go mod 的方式 -func GetPackagesDir(patterns []string) { - 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(config.GocConfig.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")) - config.GocConfig.Packages = packages - - return - } - } - } - - // 2. 要么是 import path - config.GocConfig.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 "" -} diff --git a/pkg/log/log.go b/pkg/log/log.go index 7611c55..3f44ea4 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -1,14 +1,13 @@ package log import ( - "github.com/qiniu/goc/v2/pkg/config" "go.uber.org/zap/zapcore" ) var g Logger -func NewLogger() { - if config.GocConfig.Debug == true { +func NewLogger(debug bool) { + if debug == true { g = newCiLogger() } else { g = &terminalLogger{ diff --git a/pkg/watch/watch.go b/pkg/watch/watch.go index 4177260..3cca3f8 100644 --- a/pkg/watch/watch.go +++ b/pkg/watch/watch.go @@ -4,12 +4,11 @@ import ( "fmt" "github.com/gorilla/websocket" - "github.com/qiniu/goc/v2/pkg/config" "github.com/qiniu/goc/v2/pkg/log" ) -func Watch() { - watchUrl := fmt.Sprintf("ws://%v/v2/cover/ws/watch", config.GocConfig.Host) +func Watch(host string) { + watchUrl := fmt.Sprintf("ws://%v/v2/cover/ws/watch", host) c, _, err := websocket.DefaultDialer.Dial(watchUrl, nil) if err != nil { log.Fatalf("cannot connect to goc server: %v", err)