goc/pkg/build/build_flags.go
2024-11-19 11:32:10 +08:00

504 lines
14 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
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/ar0c/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.
`
var runUsage string = `Usage:
goc run [build flags] [goc flags] [packages] [arguments...]
[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 parseroot 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 {
if _, ok := booleanFlags[f.Name]; !ok {
flags = append(flags, "-"+f.Name, f.Value.String())
} else {
flags = append(flags, "-"+f.Name)
}
if f.Name == "mod" {
if f.Value.String() == "vendor" {
b.IsVendorMod = true
} else {
b.IsVendorMod = false
}
}
}
})
b.Goflags = flags
b.CurWd = curWd
b.GoArgs = goFlagSets.Args()
return
}
func (b *Build) runCmdArgsParse() {
args := b.Args
allFlagSets := b.FlagSets
// 重写 help
helpFlag := allFlagSets.Lookup("help")
if helpFlag.Changed {
printGoHelp(runUsage)
os.Exit(0)
}
// 删除 help flag
args = findAndDelHelpFlag(args)
// 必须手动调用
// 由于关闭了 cobra 的 flag parseroot PersistentPreRun 调用时log.NewLogger 并没有拿到 debug 值
log.NewLogger(b.Debug)
curWd, err := os.Getwd()
if err != nil {
log.Fatalf("fail to get current working directory: %v", err)
}
b.CurWd = curWd
// 获取除 goc flags 之外的 args
// 删除 cobra 定义的 flag
allFlagSets.Visit(func(f *pflag.Flag) {
args = findAndDelGocFlag(args, f.Name, f.Value.String())
})
b.GoArgs = 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
}
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
var booleanFlags map[string]struct{} = make(map[string]struct{})
func addBuildFlags(cmdSet *flag.FlagSet) {
cmdSet.BoolVar(&goflags.BuildA, "a", false, "")
booleanFlags["a"] = struct{}{}
cmdSet.BoolVar(&goflags.BuildN, "n", false, "")
booleanFlags["n"] = struct{}{}
cmdSet.IntVar(&goflags.BuildP, "p", 4, "")
cmdSet.BoolVar(&goflags.BuildV, "v", false, "")
booleanFlags["v"] = struct{}{}
cmdSet.BoolVar(&goflags.BuildX, "x", false, "")
booleanFlags["x"] = struct{}{}
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, "")
booleanFlags["modcacherw"] = struct{}{}
cmdSet.StringVar(&goflags.ModFile, "modfile", "", "")
cmdSet.StringVar(&goflags.BuildLdflags, "ldflags", "", "")
cmdSet.BoolVar(&goflags.BuildLinkshared, "linkshared", false, "")
booleanFlags["linkshared"] = struct{}{}
cmdSet.StringVar(&goflags.BuildPkgdir, "pkgdir", "", "")
cmdSet.BoolVar(&goflags.BuildRace, "race", false, "")
booleanFlags["race"] = struct{}{}
cmdSet.BoolVar(&goflags.BuildMSan, "msan", false, "")
booleanFlags["msan"] = struct{}{}
cmdSet.StringVar(&goflags.BuildTags, "tags", "", "")
cmdSet.StringVar(&goflags.BuildToolexec, "toolexec", "", "")
cmdSet.BoolVar(&goflags.BuildTrimpath, "trimpath", false, "")
booleanFlags["trimpath"] = struct{}{}
cmdSet.BoolVar(&goflags.BuildWork, "work", false, "")
booleanFlags["work"] = struct{}{}
}
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 本身还判断语法上是否是同一个 packagegoc 这里不做解析
// 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 pathfile 在上一步已被排除
// 只考虑 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 ""
}