diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e4d7903796ad..7db357434ee0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -67,9 +67,7 @@ jobs: check-latest: true cache: false - name: Check Format - run: | - go install -v mvdan.cc/gofumpt@latest - go run ./infra/vformat/main.go -mode check -pwd ./ + run: go run ./infra/vformat/main.go -mode check -pwd ./ test: needs: check-assets diff --git a/go.mod b/go.mod index b4ccc086b823..be24f79b3331 100644 --- a/go.mod +++ b/go.mod @@ -37,6 +37,7 @@ require ( gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 h12.io/socks v1.0.3 lukechampine.com/blake3 v1.4.1 + mvdan.cc/gofumpt v0.12.0 ) require ( @@ -48,7 +49,6 @@ require ( github.com/juju/ratelimit v1.0.2 // indirect github.com/klauspost/compress v1.17.4 // indirect github.com/koron/go-ssdp v0.0.4 // indirect - github.com/kr/text v0.2.0 // indirect github.com/libp2p/go-netroute v0.2.1 // indirect github.com/pion/dtls/v3 v3.1.5 // indirect github.com/pion/logging v0.2.4 // indirect @@ -59,6 +59,7 @@ require ( go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/text v0.41.0 // indirect golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.49.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index d8e174e9f584..f906979488a4 100644 --- a/go.sum +++ b/go.sum @@ -6,13 +6,14 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA= github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474= +github.com/go-quicktest/qt v1.102.0/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI= github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -73,8 +74,8 @@ github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g= +github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y= github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8= @@ -147,6 +148,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -176,3 +179,5 @@ h12.io/socks v1.0.3 h1:Ka3qaQewws4j4/eDQnOdpr4wXsC//dXtWvftlIcCQUo= h12.io/socks v1.0.3/go.mod h1:AIhxy1jOId/XCz9BO+EIgNL2rQiPTBNnOfnVnQ+3Eck= lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= +mvdan.cc/gofumpt v0.12.0 h1:1Lbudkz2kpM9Cjz2pL4M19u7q+GaEhCTNf7N9mfpcho= +mvdan.cc/gofumpt v0.12.0/go.mod h1:SmBHHrljiZu/uoypeKup3rFzP6eoC9UwCp2iH5E3jZA= diff --git a/infra/vformat/main.go b/infra/vformat/main.go index 533a5a147a95..8771921eab41 100644 --- a/infra/vformat/main.go +++ b/infra/vformat/main.go @@ -1,15 +1,18 @@ package main import ( - "errors" + "bytes" "flag" "fmt" - "go/build" "os" - "os/exec" "path/filepath" "runtime" + "sort" "strings" + "sync" + "sync/atomic" + + "mvdan.cc/gofumpt/format" ) var ( @@ -23,101 +26,27 @@ var ( isFormat bool ) -// envFile returns the name of the Go environment configuration file. -// Copy from https://github.com/golang/go/blob/c4f2a9788a7be04daf931ac54382fbe2cb754938/src/cmd/go/internal/cfg/cfg.go#L150-L166 -func envFile() (string, error) { - if file := os.Getenv("GOENV"); file != "" { - if file == "off" { - return "", errors.New("GOENV=off") - } - return file, nil - } - dir, err := os.UserConfigDir() +func getModuleInfo(pwd string) (modPath, langVersion string, err error) { + data, err := os.ReadFile(filepath.Join(pwd, "go.mod")) if err != nil { - return "", err - } - if dir == "" { - return "", errors.New("missing user-config dir") + return "", "", err } - return filepath.Join(dir, "go", "env"), nil -} - -// GetRuntimeEnv returns the value of runtime environment variable, -// that is set by running following command: `go env -w key=value`. -func GetRuntimeEnv(key string) (string, error) { - file, err := envFile() - if err != nil { - return "", err - } - if file == "" { - return "", errors.New("missing runtime env file") - } - var data []byte - var runtimeEnv string - data, readErr := os.ReadFile(file) - if readErr != nil { - return "", readErr - } - envStrings := strings.Split(string(data), "\n") - for _, envItem := range envStrings { - envItem = strings.TrimSuffix(envItem, "\r") - envKeyValue := strings.Split(envItem, "=") - if len(envKeyValue) == 2 && strings.TrimSpace(envKeyValue[0]) == key { - runtimeEnv = strings.TrimSpace(envKeyValue[1]) - } - } - return runtimeEnv, nil -} - -// GetGOBIN returns GOBIN environment variable as a string. It will NOT be empty. -func GetGOBIN() string { - // The one set by user explicitly by `export GOBIN=/path` or `env GOBIN=/path command` - GOBIN := os.Getenv("GOBIN") - if GOBIN == "" { - var err error - // The one set by user by running `go env -w GOBIN=/path` - GOBIN, err = GetRuntimeEnv("GOBIN") - if err != nil { - // The default one that Golang uses - return filepath.Join(build.Default.GOPATH, "bin") - } - if GOBIN == "" { - return filepath.Join(build.Default.GOPATH, "bin") + for _, line := range strings.Split(string(data), "\n") { + fields := strings.Fields(line) + if len(fields) >= 2 { + switch fields[0] { + case "module": + modPath = fields[1] + case "go": + langVersion = "go" + strings.TrimPrefix(fields[1], "go") + } } - return GOBIN - } - return GOBIN -} - -func Run(binary string, args []string) ([]byte, error) { - cmd := exec.Command(binary, args...) - cmd.Env = append(cmd.Env, os.Environ()...) - output, cmdErr := cmd.CombinedOutput() - if cmdErr != nil { - return nil, cmdErr } - return output, nil + return modPath, langVersion, nil } -func RunMany(binary string, args, files []string) bool { - fmt.Println("Processing with", binary, args, "...") - - formatRequired := false - maxTasks := make(chan struct{}, runtime.NumCPU()) - for _, file := range files { - maxTasks <- struct{}{} - go func(file string) { - output, err := Run(binary, append(args, file)) - if err != nil { - fmt.Println(err) - } else if len(output) > 0 { - fmt.Println(string(output)) - formatRequired = true - } - <-maxTasks - }(file) - } - return formatRequired +func formatGoSource(src []byte, opts format.Options) ([]byte, error) { + return format.Source(src, opts) } func main() { @@ -150,26 +79,76 @@ func main() { } pwd := *directory - GOBIN := GetGOBIN() - binPath := os.Getenv("PATH") - pathSlice := []string{pwd, GOBIN, binPath} - binPath = strings.Join(pathSlice, string(os.PathListSeparator)) - os.Setenv("PATH", binPath) + modPath, langVersion, modErr := getModuleInfo(pwd) + if modErr != nil { + fmt.Println("Error reading go.mod:", modErr) + os.Exit(1) + } + opts := format.Options{ + LangVersion: langVersion, + ModulePath: modPath, + } - suffix := "" - if runtime.GOOS == "windows" { - suffix = ".exe" + if isFormat { + fmt.Println("Formatting Go source files...") + } else if isCheck { + fmt.Println("Checking files thar are not properly formatted...") } - gofmt := "gofumpt" + suffix - if gofmtPath, err := exec.LookPath(gofmt); err != nil { - fmt.Println("Can not find", gofmt, "in system path or current working directory.") - os.Exit(1) - } else { - gofmt = gofmtPath + jobs := make(chan string, runtime.NumCPU()) + var wg sync.WaitGroup + var formatRequired atomic.Bool + var hasErrors atomic.Bool + + for i := 0; i < runtime.NumCPU(); i++ { + wg.Go(func() { + for path := range jobs { + src, err := os.ReadFile(path) + if err != nil { + fmt.Fprintf(os.Stderr, "Error reading %s: %v\n", path, err) + hasErrors.Store(true) + continue + } + + formatted, err := formatGoSource(src, opts) + if err != nil { + fmt.Fprintf(os.Stderr, "Error formatting %s: %v\n", path, err) + hasErrors.Store(true) + continue + } + + if !bytes.Equal(src, formatted) { + var diffText []byte + if isDryrun { + newName := filepath.ToSlash(path) + oldName := newName + ".orig" + diffText = diff(oldName, src, newName, formatted) + } + if isFormat { + info, statErr := os.Stat(path) + if statErr != nil { + fmt.Fprintf(os.Stderr, "Error stating %s: %v\n", path, statErr) + hasErrors.Store(true) + continue + } + if writeErr := os.WriteFile(path, formatted, info.Mode().Perm()); writeErr != nil { + fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", path, writeErr) + hasErrors.Store(true) + continue + } + } + + formatRequired.Store(true) + if isDryrun && len(diffText) > 0 { + fmt.Printf("%s\n%s", path, diffText) + } else { + fmt.Println(path) + } + } + } + }) } - rawFilesSlice := make([]string, 0, 1000) walkErr := filepath.Walk(pwd, func(path string, info os.FileInfo, err error) error { if err != nil { fmt.Println(err) @@ -186,51 +165,206 @@ func main() { !strings.HasSuffix(filename, ".pb.go") && !strings.Contains(dir, filepath.Join("testing", "mocks")) && !strings.Contains(path, filepath.Join("main", "distro", "all", "all.go")) { - rawFilesSlice = append(rawFilesSlice, path) + jobs <- path } return nil }) + close(jobs) + wg.Wait() + if walkErr != nil { fmt.Println(walkErr) os.Exit(1) } + if hasErrors.Load() { + os.Exit(1) + } + if isFormat { - gofmtArgs := []string{ - "-l", "-e", "-w", + if formatRequired.Load() { + fmt.Println("Do NOT forget to commit file changes.") } - - fmt.Println("Formatting Go source files...") - RunMany(gofmt, gofmtArgs, rawFilesSlice) - fmt.Println("Do NOT forget to commit file changes.") } if isCheck { - gofmtListArgs := []string{ - "-l", "-e", + if formatRequired.Load() { + fmt.Println("Format problem(s) found.") + fmt.Println("Please run 'go run ./infra/vformat/main.go' to format the Go source files.") + os.Exit(1) + } else { + fmt.Println("All Go source file format check has been passed.") } + } +} - fmt.Println("Checking files thar are not properly formatted...") - formatRequired := RunMany(gofmt, gofmtListArgs, rawFilesSlice) - if formatRequired { - fmt.Println("Format problem(s) found.") +// diff algorithm copied from mvdan.cc/gofumpt/internal/govendor/diff +type pair struct{ x, y int } + +func diff(oldName string, old []byte, newName string, new []byte) []byte { + if bytes.Equal(old, new) { + return nil + } + x := diffLines(old) + y := diffLines(new) + + var out bytes.Buffer + fmt.Fprintf(&out, "diff %s %s\n", oldName, newName) + fmt.Fprintf(&out, "--- %s\n", oldName) + fmt.Fprintf(&out, "+++ %s\n", newName) + + var ( + done pair + chunk pair + count pair + ctext []string + ) + for _, m := range diffTgs(x, y) { + if m.x < done.x { + continue + } + start := m + for start.x > done.x && start.y > done.y && x[start.x-1] == y[start.y-1] { + start.x-- + start.y-- + } + end := m + for end.x < len(x) && end.y < len(y) && x[end.x] == y[end.y] { + end.x++ + end.y++ } - if isDryrun { - if formatRequired { - gofmtShowArgs := []string{ - "-d", "-e", - } - RunMany(gofmt, gofmtShowArgs, rawFilesSlice) + for _, s := range x[done.x:start.x] { + ctext = append(ctext, "-"+s) + count.x++ + } + for _, s := range y[done.y:start.y] { + ctext = append(ctext, "+"+s) + count.y++ + } + + const C = 3 + if (end.x < len(x) || end.y < len(y)) && + (end.x-start.x < C || (len(ctext) > 0 && end.x-start.x < 2*C)) { + for _, s := range x[start.x:end.x] { + ctext = append(ctext, " "+s) + count.x++ + count.y++ } + done = end + continue } - if formatRequired { - fmt.Println("Please run 'go install -v mvdan.cc/gofumpt@latest', then run 'go run ./infra/vformat/main.go' to format the Go source files.") - os.Exit(1) - } else { - fmt.Println("All Go source file format check has been passed.") + if len(ctext) > 0 { + n := end.x - start.x + if n > C { + n = C + } + for _, s := range x[start.x : start.x+n] { + ctext = append(ctext, " "+s) + count.x++ + count.y++ + } + done = pair{start.x + n, start.y + n} + + if count.x > 0 { + chunk.x++ + } + if count.y > 0 { + chunk.y++ + } + fmt.Fprintf(&out, "@@ -%d,%d +%d,%d @@\n", chunk.x, count.x, chunk.y, count.y) + for _, s := range ctext { + out.WriteString(s) + } + count.x = 0 + count.y = 0 + ctext = ctext[:0] + } + + if end.x >= len(x) && end.y >= len(y) { + break + } + + chunk = pair{end.x - C, end.y - C} + for _, s := range x[chunk.x:end.x] { + ctext = append(ctext, " "+s) + count.x++ + count.y++ + } + done = end + } + + return out.Bytes() +} + +func diffLines(x []byte) []string { + l := strings.SplitAfter(string(x), "\n") + if l[len(l)-1] == "" { + l = l[:len(l)-1] + } else { + l[len(l)-1] += "\n\\ No newline at end of file\n" + } + return l +} + +func diffTgs(x, y []string) []pair { + m := make(map[string]int) + for _, s := range x { + if c := m[s]; c > -2 { + m[s] = c - 1 + } + } + for _, s := range y { + if c := m[s]; c > -8 { + m[s] = c - 4 + } + } + + var xi, yi, inv []int + for i, s := range y { + if m[s] == -5 { + m[s] = len(yi) + yi = append(yi, i) + } + } + for i, s := range x { + if j, ok := m[s]; ok && j >= 0 { + xi = append(xi, i) + inv = append(inv, j) + } + } + + J := inv + n := len(xi) + T := make([]int, n) + L := make([]int, n) + for i := range T { + T[i] = n + 1 + } + for i := 0; i < n; i++ { + k := sort.Search(n, func(k int) bool { + return T[k] >= J[i] + }) + T[k] = J[i] + L[i] = k + 1 + } + k := 0 + for _, v := range L { + if k < v { + k = v + } + } + seq := make([]pair, 2+k) + seq[1+k] = pair{len(x), len(y)} + lastj := n + for i := n - 1; i >= 0; i-- { + if L[i] == k && J[i] < lastj { + seq[k] = pair{xi[i], yi[J[i]]} + k-- } } + seq[0] = pair{0, 0} + return seq } diff --git a/transport/pipe/pipe.go b/transport/pipe/pipe.go index 6343adb7f038..186d71b286a7 100644 --- a/transport/pipe/pipe.go +++ b/transport/pipe/pipe.go @@ -63,8 +63,8 @@ func New(opts ...Option) (*Reader, *Writer) { } return &Reader{ - pipe: p, - }, &Writer{ - pipe: p, - } + pipe: p, + }, &Writer{ + pipe: p, + } }