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  }