github.com/v2fly/v2ray-core/v5@v5.16.2-0.20240507031116-8191faa6e095/infra/vprotogen/main.go (about) 1 package main 2 3 import ( 4 "bufio" 5 "bytes" 6 "fmt" 7 "go/build" 8 "io" 9 "net/http" 10 "os" 11 "os/exec" 12 "path/filepath" 13 "regexp" 14 "runtime" 15 "strconv" 16 "strings" 17 ) 18 19 // envFile returns the name of the Go environment configuration file. 20 // Copy from https://github.com/golang/go/blob/c4f2a9788a7be04daf931ac54382fbe2cb754938/src/cmd/go/internal/cfg/cfg.go#L150-L166 21 func envFile() (string, error) { 22 if file := os.Getenv("GOENV"); file != "" { 23 if file == "off" { 24 return "", fmt.Errorf("GOENV=off") 25 } 26 return file, nil 27 } 28 dir, err := os.UserConfigDir() 29 if err != nil { 30 return "", err 31 } 32 if dir == "" { 33 return "", fmt.Errorf("missing user-config dir") 34 } 35 return filepath.Join(dir, "go", "env"), nil 36 } 37 38 // GetRuntimeEnv returns the value of runtime environment variable, 39 // that is set by running following command: `go env -w key=value`. 40 func GetRuntimeEnv(key string) (string, error) { 41 file, err := envFile() 42 if err != nil { 43 return "", err 44 } 45 if file == "" { 46 return "", fmt.Errorf("missing runtime env file") 47 } 48 var data []byte 49 var runtimeEnv string 50 data, readErr := os.ReadFile(file) 51 if readErr != nil { 52 return "", readErr 53 } 54 envStrings := strings.Split(string(data), "\n") 55 for _, envItem := range envStrings { 56 envItem = strings.TrimSuffix(envItem, "\r") 57 envKeyValue := strings.Split(envItem, "=") 58 if strings.EqualFold(strings.TrimSpace(envKeyValue[0]), key) { 59 runtimeEnv = strings.TrimSpace(envKeyValue[1]) 60 } 61 } 62 return runtimeEnv, nil 63 } 64 65 // GetGOBIN returns GOBIN environment variable as a string. It will NOT be empty. 66 func GetGOBIN() string { 67 // The one set by user explicitly by `export GOBIN=/path` or `env GOBIN=/path command` 68 GOBIN := os.Getenv("GOBIN") 69 if GOBIN == "" { 70 var err error 71 // The one set by user by running `go env -w GOBIN=/path` 72 GOBIN, err = GetRuntimeEnv("GOBIN") 73 if err != nil { 74 // The default one that Golang uses 75 return filepath.Join(build.Default.GOPATH, "bin") 76 } 77 if GOBIN == "" { 78 return filepath.Join(build.Default.GOPATH, "bin") 79 } 80 return GOBIN 81 } 82 return GOBIN 83 } 84 85 func whichProtoc(suffix, targetedVersion string) (string, error) { 86 protoc := "protoc" + suffix 87 88 path, err := exec.LookPath(protoc) 89 if err != nil { 90 errStr := fmt.Sprintf(` 91 Command "%s" not found. 92 Make sure that %s is in your system path or current path. 93 Download %s v%s or later from https://github.com/protocolbuffers/protobuf/releases 94 `, protoc, protoc, protoc, targetedVersion) 95 return "", fmt.Errorf(errStr) 96 } 97 return path, nil 98 } 99 100 func getProjectProtocVersion(url string) (string, error) { 101 resp, err := http.Get(url) 102 if err != nil { 103 return "", fmt.Errorf("can not get the version of protobuf used in V2Ray project") 104 } 105 defer resp.Body.Close() 106 body, err := io.ReadAll(resp.Body) 107 if err != nil { 108 return "", fmt.Errorf("can not read from body") 109 } 110 versionRegexp := regexp.MustCompile(`\/\/\s*protoc\s*v(\d+\.\d+\.\d+)`) 111 matched := versionRegexp.FindStringSubmatch(string(body)) 112 return matched[1], nil 113 } 114 115 func getInstalledProtocVersion(protocPath string) (string, error) { 116 cmd := exec.Command(protocPath, "--version") 117 cmd.Env = append(cmd.Env, os.Environ()...) 118 output, cmdErr := cmd.CombinedOutput() 119 if cmdErr != nil { 120 return "", cmdErr 121 } 122 versionRegexp := regexp.MustCompile(`protoc\s*(\d+\.\d+(\.\d)*)`) 123 matched := versionRegexp.FindStringSubmatch(string(output)) 124 return matched[1], nil 125 } 126 127 func parseVersion(s string, width int) int64 { 128 strList := strings.Split(s, ".") 129 format := fmt.Sprintf("%%s%%0%ds", width) 130 v := "" 131 if len(strList) == 2 { 132 strList = append([]string{"4"}, strList...) 133 } 134 for _, value := range strList { 135 v = fmt.Sprintf(format, v, value) 136 } 137 var result int64 138 var err error 139 if result, err = strconv.ParseInt(v, 10, 64); err != nil { 140 return 0 141 } 142 return result 143 } 144 145 func needToUpdate(targetedVersion, installedVersion string) bool { 146 vt := parseVersion(targetedVersion, 4) 147 vi := parseVersion(installedVersion, 4) 148 return vt > vi 149 } 150 151 func main() { 152 pwd, err := os.Getwd() 153 if err != nil { 154 fmt.Println("Can not get current working directory.") 155 os.Exit(1) 156 } 157 158 GOBIN := GetGOBIN() 159 binPath := os.Getenv("PATH") 160 pathSlice := []string{pwd, GOBIN, binPath} 161 binPath = strings.Join(pathSlice, string(os.PathListSeparator)) 162 os.Setenv("PATH", binPath) 163 164 suffix := "" 165 if runtime.GOOS == "windows" { 166 suffix = ".exe" 167 } 168 169 targetedVersion, err := getProjectProtocVersion("https://raw.githubusercontent.com/v2fly/v2ray-core/HEAD/config.pb.go") 170 if err != nil { 171 fmt.Println(err) 172 os.Exit(1) 173 } 174 175 protoc, err := whichProtoc(suffix, targetedVersion) 176 if err != nil { 177 fmt.Println(err) 178 os.Exit(1) 179 } 180 181 if linkPath, err := os.Readlink(protoc); err == nil { 182 protoc = linkPath 183 } 184 185 installedVersion, err := getInstalledProtocVersion(protoc) 186 if err != nil { 187 fmt.Println(err) 188 os.Exit(1) 189 } 190 191 if needToUpdate(targetedVersion, installedVersion) { 192 fmt.Printf(` 193 You are using an old protobuf version, please update to v%s or later. 194 Download it from https://github.com/protocolbuffers/protobuf/releases 195 196 * Protobuf version used in V2Ray project: v%s 197 * Protobuf version you have installed: v%s 198 199 `, targetedVersion, targetedVersion, installedVersion) 200 os.Exit(1) 201 } 202 203 protoFilesMap := make(map[string][]string) 204 walkErr := filepath.Walk("./", func(path string, info os.FileInfo, err error) error { 205 if err != nil { 206 fmt.Println(err) 207 return err 208 } 209 210 if info.IsDir() { 211 return nil 212 } 213 214 dir := filepath.Dir(path) 215 filename := filepath.Base(path) 216 if strings.HasSuffix(filename, ".proto") && 217 filename != "typed_message.proto" && 218 filename != "descriptor.proto" { 219 protoFilesMap[dir] = append(protoFilesMap[dir], path) 220 } 221 222 return nil 223 }) 224 if walkErr != nil { 225 fmt.Println(walkErr) 226 os.Exit(1) 227 } 228 229 for _, files := range protoFilesMap { 230 for _, relProtoFile := range files { 231 args := []string{ 232 "-I", fmt.Sprintf("%v/../include", filepath.Dir(protoc)), 233 "-I", ".", 234 "--go_out", pwd, 235 "--go_opt", "paths=source_relative", 236 "--go-grpc_out", pwd, 237 "--go-grpc_opt", "paths=source_relative", 238 "--plugin", "protoc-gen-go=" + filepath.Join(GOBIN, "protoc-gen-go"+suffix), 239 "--plugin", "protoc-gen-go-grpc=" + filepath.Join(GOBIN, "protoc-gen-go-grpc"+suffix), 240 } 241 args = append(args, relProtoFile) 242 cmd := exec.Command(protoc, args...) 243 cmd.Env = append(cmd.Env, os.Environ()...) 244 output, cmdErr := cmd.CombinedOutput() 245 if len(output) > 0 { 246 fmt.Println(string(output)) 247 } 248 if cmdErr != nil { 249 fmt.Println(cmdErr) 250 os.Exit(1) 251 } 252 } 253 } 254 255 normalizeWalkErr := filepath.Walk("./", func(path string, info os.FileInfo, err error) error { 256 if err != nil { 257 fmt.Println(err) 258 return err 259 } 260 261 if info.IsDir() { 262 return nil 263 } 264 265 filename := filepath.Base(path) 266 if strings.HasSuffix(filename, ".pb.go") && 267 path != "config.pb.go" { 268 if err := NormalizeGeneratedProtoFile(path); err != nil { 269 fmt.Println(err) 270 os.Exit(1) 271 } 272 } 273 274 return nil 275 }) 276 if normalizeWalkErr != nil { 277 fmt.Println(normalizeWalkErr) 278 os.Exit(1) 279 } 280 } 281 282 func NormalizeGeneratedProtoFile(path string) error { 283 fd, err := os.OpenFile(path, os.O_RDWR, 0o644) 284 if err != nil { 285 return err 286 } 287 288 _, err = fd.Seek(0, os.SEEK_SET) 289 if err != nil { 290 return err 291 } 292 out := bytes.NewBuffer(nil) 293 scanner := bufio.NewScanner(fd) 294 valid := false 295 for scanner.Scan() { 296 if !valid && !strings.HasPrefix(scanner.Text(), "package ") { 297 continue 298 } 299 valid = true 300 out.Write(scanner.Bytes()) 301 out.Write([]byte("\n")) 302 } 303 _, err = fd.Seek(0, os.SEEK_SET) 304 if err != nil { 305 return err 306 } 307 err = fd.Truncate(0) 308 if err != nil { 309 return err 310 } 311 _, err = io.Copy(fd, bytes.NewReader(out.Bytes())) 312 if err != nil { 313 return err 314 } 315 return nil 316 }