github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/tm2/pkg/amino/genproto/genproto.go (about) 1 package genproto 2 3 // p3c.SetProjectRootGopkg("example.com/main") 4 5 import ( 6 "bytes" 7 "errors" 8 "fmt" 9 "os" 10 "os/exec" 11 "path" 12 "path/filepath" 13 "reflect" 14 "strings" 15 "time" 16 17 "github.com/gnolang/gno/tm2/pkg/amino" 18 "github.com/gnolang/gno/tm2/pkg/amino/genproto/stringutil" 19 "github.com/gnolang/gno/tm2/pkg/amino/pkg" 20 ) 21 22 // TODO sort 23 // * Proto3 import file paths are by default always full (including 24 // domain) and basically the gopkg path. This lets proto3 schema 25 // import paths stay consistent even as dependency. 26 // * In the go mod world, the user is expected to run an independent 27 // tool to copy proto files to a proto folder from go mod dependencies. 28 // This is provided by MakeProtoFilder(). 29 30 // P3Context holds contextual information beyond the P3Doc. 31 // 32 // It holds all the package infos needed to derive the full P3doc, 33 // including p3 import paths, as well as where to write them, 34 // because all of that information is encapsulated in amino.Package. 35 // 36 // It also holds a local amino.Codec instance, with package registrations 37 // passed through. 38 type P3Context struct { 39 // e.g. "github.com/tendermint/tendermint/abci/types" -> 40 // &Package{...} 41 packages pkg.PackageSet 42 43 // TODO 44 // // for beyond default "type.proto" 45 // // e.g. "tendermint.abci.types" -> 46 // // []string{"github.com/tendermint/abci/types/types.proto"}} 47 // moreP3Imports map[string][]string 48 49 // This is only necessary to construct TypeInfo. 50 cdc *amino.Codec 51 } 52 53 func NewP3Context() *P3Context { 54 p3c := &P3Context{ 55 packages: pkg.NewPackageSet(), 56 cdc: amino.NewCodec(), 57 } 58 return p3c 59 } 60 61 func (p3c *P3Context) RegisterPackage(pkg *amino.Package) { 62 pkgs := pkg.CrawlPackages(nil) 63 for _, pkg := range pkgs { 64 p3c.registerPackage(pkg) 65 } 66 } 67 68 func (p3c *P3Context) registerPackage(pkg *amino.Package) { 69 p3c.packages.Add(pkg) 70 p3c.cdc.RegisterPackage(pkg) 71 } 72 73 func (p3c *P3Context) GetPackage(gopkg string) *amino.Package { 74 return p3c.packages.Get(gopkg) 75 } 76 77 // Crawls the packages and flattens all dependencies. 78 // Includes 79 func (p3c *P3Context) GetAllPackages() (res []*amino.Package) { 80 seen := map[*amino.Package]struct{}{} 81 for _, pkg := range p3c.packages { 82 pkgs := pkg.CrawlPackages(seen) 83 res = append(res, pkgs...) 84 } 85 for _, pkg := range p3c.cdc.GetPackages() { 86 if _, exists := seen[pkg]; !exists { 87 res = append(res, pkg) 88 } 89 } 90 return 91 } 92 93 func (p3c *P3Context) ValidateBasic() { 94 // TODO: do verifications across packages. 95 // pkgs := p3c.GetAllPackages() 96 } 97 98 // TODO: This could live as a method of the package, and only crawl the 99 // dependencies of that package. But a method implemented on P3Context 100 // should function like this and print an intelligent error. 101 // Set implicit to false to assert-that name matches in package. 102 // Set implicit to true for implicit structures like nested lists. 103 func (p3c *P3Context) GetP3ImportPath(p3type P3Type, implicit bool) string { 104 p3pkg := p3type.GetPackageName() 105 pkgs := p3c.GetAllPackages() 106 for _, pkg := range pkgs { 107 if pkg.P3PkgName == p3pkg { 108 if implicit { 109 return pkg.P3ImportPath 110 } else if pkg.HasName(p3type.GetName()) { 111 return pkg.P3ImportPath 112 } 113 } 114 } 115 panic(fmt.Sprintf("proto3 type %v not recognized", p3type)) 116 } 117 118 // Given a codec and some reflection type, generate the Proto3 message 119 // (partial) schema. Imports are added to p3doc. 120 func (p3c *P3Context) GenerateProto3MessagePartial(p3doc *P3Doc, rt reflect.Type) (p3msg P3Message) { 121 if p3doc.PackageName == "" { 122 panic(fmt.Sprintf("cannot generate message partials in the root package \"\".")) 123 } 124 if rt.Kind() == reflect.Ptr { 125 panic("pointers not yet supported. if you meant pointer-preferred (for decoding), pass in rt.Elem()") 126 } 127 if rt.Kind() == reflect.Interface { 128 panic("nothing to generate for interfaces") 129 } 130 131 info, err := p3c.cdc.GetTypeInfo(rt) 132 if err != nil { 133 panic(err) 134 } 135 136 // The p3 schema is determined by the structure of ReprType. But the name, 137 // package, and where the binding artifacts get written, are all of the 138 // original package. Thus, .ReprType.Type.Name() and 139 // .ReprType.Type.Package etc should not be used, and sometimes we must 140 // preserve the original info's package as arguments along with .ReprType. 141 rinfo := info.ReprType 142 if rinfo.ReprType != rinfo { 143 // info.ReprType should point to itself, chaining is not allowed. 144 panic("should not happen") 145 } 146 147 rsfields := []amino.FieldInfo(nil) 148 if rinfo.Type.Kind() == reflect.Struct { 149 switch rinfo.Type { 150 case timeType: 151 // special case: time 152 rinfo, err := p3c.cdc.GetTypeInfo(gTimestampType) 153 if err != nil { 154 panic(err) 155 } 156 rsfields = rinfo.StructInfo.Fields 157 case durationType: 158 // special case: duration 159 rinfo, err := p3c.cdc.GetTypeInfo(gDurationType) 160 if err != nil { 161 panic(err) 162 } 163 rsfields = rinfo.StructInfo.Fields 164 default: 165 // general case 166 rsfields = rinfo.StructInfo.Fields 167 } 168 } else { 169 // implicit struct. 170 // TODO: shouldn't this name end with "Wrapper" suffix? 171 rsfields = []amino.FieldInfo{{ 172 Type: rinfo.Type, 173 TypeInfo: rinfo, 174 Name: "Value", 175 FieldOptions: amino.FieldOptions{ 176 // TODO can we override JSON to unwrap here? 177 BinFieldNum: 1, 178 }, 179 }} 180 } 181 182 // When fields include other declared structs, 183 // we need to know whether it's an external reference 184 // (with corresponding imports in the proto3 schema) 185 // or an internal reference (with no imports necessary). 186 pkgPath := rt.PkgPath() 187 if pkgPath == "" { 188 panic(fmt.Errorf("can only generate proto3 message schemas from user-defined package-level declared structs, got rt %v", rt)) 189 } 190 191 p3msg.Name = info.Name // not rinfo. 192 193 var fieldComments map[string]string 194 if rinfo.Package != nil { 195 if pkgType, ok := rinfo.Package.GetType(rt); ok { 196 p3msg.Comment = pkgType.Comment 197 // We will check for optional field comments below. 198 fieldComments = pkgType.FieldComments 199 } 200 } 201 202 // Append to p3msg.Fields, fields of the struct. 203 for _, field := range rsfields { // rinfo. 204 fp3, fp3IsRepeated, implicit := typeToP3Type(info.Package, field.TypeInfo, field.FieldOptions) 205 // If the p3 field package is the same, omit the prefix. 206 if fp3.GetPackageName() == p3doc.PackageName { 207 fp3m := fp3.(P3MessageType) 208 fp3m.SetOmitPackage() 209 fp3 = fp3m 210 } else if fp3.GetPackageName() != "" { 211 importPath := p3c.GetP3ImportPath(fp3, implicit) 212 p3doc.AddImport(importPath) 213 } 214 p3Field := P3Field{ 215 Repeated: fp3IsRepeated, 216 Type: fp3, 217 Name: stringutil.ToLowerSnakeCase(field.Name), 218 JSONName: field.JSONName, 219 Number: field.FieldOptions.BinFieldNum, 220 } 221 if fieldComments != nil { 222 p3Field.Comment = fieldComments[field.Name] 223 } 224 p3msg.Fields = append(p3msg.Fields, p3Field) 225 } 226 227 return 228 } 229 230 // Generate the Proto3 message (partial) schema for an implist list. Imports 231 // are added to p3doc. 232 func (p3c *P3Context) GenerateProto3ListPartial(p3doc *P3Doc, nl NList) (p3msg P3Message) { 233 if p3doc.PackageName == "" { 234 panic(fmt.Sprintf("cannot generate message partials in the root package \"\".")) 235 } 236 237 ep3 := nl.ElemP3Type() 238 if ep3.GetPackageName() == p3doc.PackageName { 239 ep3m := ep3.(P3MessageType) 240 ep3m.SetOmitPackage() 241 ep3 = ep3m 242 } 243 p3Field := P3Field{ 244 Repeated: true, 245 Type: ep3, 246 Name: "Value", 247 Number: 1, 248 } 249 p3msg.Name = nl.Name() 250 p3msg.Fields = append(p3msg.Fields, p3Field) 251 return 252 } 253 254 // Given the arguments, create a new P3Doc. 255 // pkg is optional. 256 func (p3c *P3Context) GenerateProto3SchemaForTypes(pkg *amino.Package, rtz ...reflect.Type) (p3doc P3Doc) { 257 if pkg.P3PkgName == "" { 258 panic(errors.New("cannot generate schema in the root package \"\"")) 259 } 260 261 // Set the package. 262 p3doc.PackageName = pkg.P3PkgName 263 p3doc.GoPackage = pkg.P3GoPkgPath 264 265 // Add declared imports. 266 for _, dep := range pkg.GetAllDependencies() { 267 p3doc.AddImport(dep.P3ImportPath) 268 } 269 270 // Set Message schemas. 271 for _, rt := range rtz { 272 if rt.Kind() == reflect.Interface { 273 continue 274 } else if rt.Kind() == reflect.Ptr { 275 rt = rt.Elem() 276 } 277 p3msg := p3c.GenerateProto3MessagePartial(&p3doc, rt) 278 p3doc.Messages = append(p3doc.Messages, p3msg) 279 } 280 281 // Collect list types and uniq, 282 // then create list message schemas. 283 // These are representational 284 nestedListTypes := make(map[string]NList) 285 for _, rt := range rtz { 286 if rt.Kind() == reflect.Interface { 287 continue 288 } 289 info, err := p3c.cdc.GetTypeInfo(rt) 290 if err != nil { 291 panic(err) 292 } 293 findNLists(pkg, info, &nestedListTypes) 294 } 295 for _, nl := range sortFound(nestedListTypes) { 296 p3msg := p3c.GenerateProto3ListPartial(&p3doc, nl) 297 p3doc.Messages = append(p3doc.Messages, p3msg) 298 } 299 300 return p3doc 301 } 302 303 // Convenience. 304 func (p3c *P3Context) WriteProto3SchemaForTypes(filename string, pkg *amino.Package, rtz ...reflect.Type) { 305 fmt.Printf("writing proto3 schema to %v for package %v\n", filename, pkg) 306 p3doc := p3c.GenerateProto3SchemaForTypes(pkg, rtz...) 307 err := os.WriteFile(filename, []byte(p3doc.Print()), 0o644) 308 if err != nil { 309 panic(err) 310 } 311 } 312 313 var ( 314 timeType = reflect.TypeOf(time.Now()) 315 durationType = reflect.TypeOf(time.Duration(0)) 316 ) 317 318 // If info.ReprType is a struct, the returned proto3 type is a P3MessageType. 319 func typeToP3Type(root *amino.Package, info *amino.TypeInfo, fopts amino.FieldOptions) (p3type P3Type, repeated bool, implicit bool) { 320 // Special case overrides. 321 // We don't handle the case when info.ReprType.Type is time here. 322 switch info.Type { 323 case timeType: 324 return NewP3MessageType("google.protobuf", "Timestamp"), false, false 325 case durationType: 326 return NewP3MessageType("google.protobuf", "Duration"), false, false 327 } 328 329 // Dereference type, in case pointer. 330 rt := info.ReprType.Type 331 switch rt.Kind() { 332 case reflect.Interface: 333 return P3AnyType, false, false 334 case reflect.Bool: 335 return P3ScalarTypeBool, false, false 336 case reflect.Int: 337 if fopts.BinFixed64 { 338 return P3ScalarTypeSfixed64, false, false 339 } else if fopts.BinFixed32 { 340 return P3ScalarTypeSfixed32, false, false 341 } else { 342 return P3ScalarTypeSint64, false, false 343 } 344 case reflect.Int8: 345 return P3ScalarTypeSint32, false, false 346 case reflect.Int16: 347 return P3ScalarTypeSint32, false, false 348 case reflect.Int32: 349 if fopts.BinFixed32 { 350 return P3ScalarTypeSfixed32, false, false 351 } else { 352 return P3ScalarTypeSint32, false, false 353 } 354 case reflect.Int64: 355 if fopts.BinFixed64 { 356 return P3ScalarTypeSfixed64, false, false 357 } else { 358 return P3ScalarTypeSint64, false, false 359 } 360 case reflect.Uint: 361 if fopts.BinFixed64 { 362 return P3ScalarTypeFixed64, false, false 363 } else if fopts.BinFixed32 { 364 return P3ScalarTypeFixed32, false, false 365 } else { 366 return P3ScalarTypeUint64, false, false 367 } 368 case reflect.Uint8: 369 return P3ScalarTypeUint32, false, false 370 case reflect.Uint16: 371 return P3ScalarTypeUint32, false, false 372 case reflect.Uint32: 373 if fopts.BinFixed32 { 374 return P3ScalarTypeFixed32, false, false 375 } else { 376 return P3ScalarTypeUint32, false, false 377 } 378 case reflect.Uint64: 379 if fopts.BinFixed64 { 380 return P3ScalarTypeFixed64, false, false 381 } else { 382 return P3ScalarTypeUint64, false, false 383 } 384 case reflect.Float32: 385 return P3ScalarTypeFloat, false, false 386 case reflect.Float64: 387 return P3ScalarTypeDouble, false, false 388 case reflect.Complex64, reflect.Complex128: 389 panic("complex types not yet supported") 390 case reflect.Array, reflect.Slice: 391 switch info.Elem.ReprType.Type.Kind() { 392 case reflect.Uint8: 393 return P3ScalarTypeBytes, false, false 394 default: 395 elemP3Type, elemRepeated, _ := typeToP3Type(root, info.Elem, fopts) 396 if elemRepeated { 397 elemP3Type = newNList(root, info, fopts).ElemP3Type() 398 return elemP3Type, true, true 399 } 400 return elemP3Type, true, false 401 } 402 case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, 403 reflect.UnsafePointer: 404 panic("chan, func, map, and pointers are not supported") 405 case reflect.String: 406 return P3ScalarTypeString, false, false 407 case reflect.Struct: 408 if info.Package == nil { 409 panic(fmt.Sprintf("type %v not registered with codec", info.Type.Name())) 410 } 411 // NOTE: we don't use rt, because the p3 package and name should still 412 // match the declaration, rather than inherit or refer to the repr type 413 // (if it is registered at all). 414 return NewP3MessageType(info.Package.P3PkgName, info.Name), false, false 415 default: 416 panic("unexpected rt kind") 417 } 418 } 419 420 // Writes in the same directory as the origin package. 421 func WriteProto3Schema(pkg *amino.Package) { 422 p3c := NewP3Context() 423 p3c.RegisterPackage(pkg) 424 p3c.ValidateBasic() 425 filename := path.Join(pkg.DirName, pkg.GoPkgName+".proto") 426 p3c.WriteProto3SchemaForTypes(filename, pkg, pkg.ReflectTypes()...) 427 } 428 429 // Symlinks .proto files from pkg info to dirname, keeping the go path 430 // structure as expected, <dirName>/path/to/gopkg/<gopkgName>.proto. 431 // If Pkg.DirName is empty, the package is considered "well known", and 432 // the mapping is not made. 433 func MakeProtoFolder(pkg *amino.Package, dirName string) { 434 fmt.Printf("making proto3 schema folder for package %v\n", pkg) 435 p3c := NewP3Context() 436 p3c.RegisterPackage(pkg) 437 438 // Populate mapping. 439 // p3 import path -> p3 import file (abs path). 440 // e.g. "github.com/.../mygopkg.proto" -> 441 // "/gopath/pkg/mod/.../mygopkg.proto" 442 p3imports := map[string]string{} 443 for _, dpkg := range p3c.GetAllPackages() { 444 if dpkg.P3SchemaFile == "" { 445 // Skip well known packages like google.protobuf.Any 446 continue 447 } 448 p3path := dpkg.P3ImportPath 449 if p3path == "" { 450 panic("P3ImportPath cannot be empty") 451 } 452 p3file := dpkg.P3SchemaFile 453 p3imports[p3path] = p3file 454 } 455 456 // Check validity. 457 if _, err := os.Stat(dirName); os.IsNotExist(err) { 458 panic(fmt.Sprintf("directory %v does not exist", dirName)) 459 } 460 461 // Make symlinks. 462 for p3path, p3file := range p3imports { 463 fmt.Println("p3path", p3path, "p3file", p3file) 464 loc := path.Join(dirName, p3path) 465 locdir := path.Dir(loc) 466 // Ensure that paths exist. 467 if _, err := os.Stat(locdir); os.IsNotExist(err) { 468 err = os.MkdirAll(locdir, os.ModePerm) 469 if err != nil { 470 panic(err) 471 } 472 } 473 // Delete existing symlink. 474 if _, err := os.Stat(loc); !os.IsNotExist(err) { 475 err := os.Remove(loc) 476 if err != nil { 477 panic(err) 478 } 479 } 480 // Write symlink. 481 err := os.Symlink(p3file, loc) 482 if os.IsExist(err) { 483 // do nothing. 484 } else if err != nil { 485 panic(err) 486 } 487 } 488 } 489 490 // Uses pkg.P3GoPkgPath to determine where the compiled file goes. If 491 // pkg.P3GoPkgPath is a subpath of pkg.GoPkgPath, then it will be 492 // written in the relevant subpath in pkg.DirName. 493 // `protosDir`: folder where .proto files for all dependencies live. 494 func RunProtoc(pkg *amino.Package, protosDir string) { 495 if !strings.HasSuffix(pkg.P3SchemaFile, ".proto") { 496 panic(fmt.Sprintf("expected P3Importfile to have .proto suffix, got %v", pkg.P3SchemaFile)) 497 } 498 inDir := filepath.Dir(pkg.P3SchemaFile) 499 inFile := filepath.Base(pkg.P3SchemaFile) 500 outDir := path.Join(inDir, "pb") 501 outFile := inFile[:len(inFile)-6] + ".pb.go" 502 // Ensure that paths exist. 503 if _, err := os.Stat(outDir); os.IsNotExist(err) { 504 err = os.MkdirAll(outDir, os.ModePerm) 505 if err != nil { 506 panic(err) 507 } 508 } 509 // First generate output to a temp dir. 510 tempDir, err := os.MkdirTemp("", "amino-genproto") 511 if err != nil { 512 return 513 } 514 // Run protoc 515 cmd := exec.Command("protoc", "-I="+inDir, "-I="+protosDir, "--go_out="+tempDir, pkg.P3SchemaFile) 516 fmt.Println("running protoc: ", cmd.String()) 517 cmd.Stdin = nil 518 var out bytes.Buffer 519 cmd.Stdout = &out 520 cmd.Stderr = &out 521 err = cmd.Run() 522 if err != nil { 523 fmt.Println("ERROR: ", out.String()) 524 panic(err) 525 } 526 527 // Copy file from tempDir to outDir. 528 copyFile( 529 path.Join(tempDir, pkg.P3GoPkgPath, outFile), 530 path.Join(outDir, outFile), 531 ) 532 } 533 534 func copyFile(src string, dst string) { 535 // Read all content of src to data 536 data, err := os.ReadFile(src) 537 if err != nil { 538 panic(err) 539 } 540 // Write data to dst 541 err = os.WriteFile(dst, data, 0o644) 542 if err != nil { 543 panic(err) 544 } 545 }