Implement CIDLC support for package imports
This change correctly implements package/module resolution in CIDLC. For now, this only works for intra-package imports, which is sufficient for now. Eventually we will need to support this (see pulumi/coconut#138).
This commit is contained in:
parent
46227870e4
commit
af3949509a
5 changed files with 389 additions and 152 deletions
|
@ -3,9 +3,6 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
@ -17,42 +14,47 @@ import (
|
|||
|
||||
func NewCIDLCCmd() *cobra.Command {
|
||||
var logToStderr bool
|
||||
var name string
|
||||
var outPack string
|
||||
var outRPC string
|
||||
var pkgBase string
|
||||
var root string
|
||||
var pkgBaseIDL string
|
||||
var pkgBaseRPC string
|
||||
var quiet bool
|
||||
var recurse bool
|
||||
var verbose int
|
||||
cmd := &cobra.Command{
|
||||
Use: "cidlc --name <name> [paths...]",
|
||||
Use: "cidlc [pkg-name] [idl-path]",
|
||||
Short: "CIDLC generates Coconut metadata and RPC stubs from IDL written in Go",
|
||||
Long: "CIDLC generates Coconut metadata and RPC stubs from IDL written in Go.\n" +
|
||||
"\n" +
|
||||
"The tool accepts a subset of Go types and produces packages that can be consumed by\n" +
|
||||
"ordinary Coconut programs and libraries in any language. The [pkg-name] argument\n" +
|
||||
"controls the output package name and [idl-path] is the path to the IDL sourcecode.\n" +
|
||||
"\n" +
|
||||
"The --out-pack and --out-rpc flags indicate where generated code is to be saved,\n" +
|
||||
"and pkg-base-idl and --pkg-base-rpc may be used to override the default inferred Go\n" +
|
||||
"package names (which, by default, are based on your GOPATH).",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.InitLogging(logToStderr, verbose)
|
||||
},
|
||||
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
|
||||
if name == "" {
|
||||
return errors.New("missing required package name (--name or -n)")
|
||||
} else if pkgBase == "" {
|
||||
return errors.New("missing required package base (--pkg-base or -p)")
|
||||
}
|
||||
if root == "" {
|
||||
root, _ = os.Getwd() // default to the current working directory.
|
||||
} else {
|
||||
root, _ = filepath.Abs(root)
|
||||
}
|
||||
if outPack != "" {
|
||||
outPack, _ = filepath.Abs(outPack)
|
||||
}
|
||||
if outRPC != "" {
|
||||
outRPC, _ = filepath.Abs(outRPC)
|
||||
if len(args) == 0 {
|
||||
return cmd.Usage()
|
||||
} else if len(args) == 1 {
|
||||
return errors.New("missing required [idl-path] argument")
|
||||
}
|
||||
|
||||
// Now pass the arguments and compile the package.
|
||||
name := args[0] // the name of the Coconut package.
|
||||
path := args[1] // the path to the IDL directory that is compiled recursively.
|
||||
return cidlc.Compile(cidlc.CompileOptions{
|
||||
Name: tokens.PackageName(name),
|
||||
Root: root,
|
||||
OutPack: outPack,
|
||||
OutRPC: outRPC,
|
||||
}, args...)
|
||||
Name: tokens.PackageName(name),
|
||||
PkgBaseIDL: pkgBaseIDL,
|
||||
PkgBaseRPC: pkgBaseRPC,
|
||||
OutPack: outPack,
|
||||
OutRPC: outRPC,
|
||||
Quiet: quiet,
|
||||
Recurse: recurse,
|
||||
}, path)
|
||||
}),
|
||||
PersistentPostRun: func(cmd *cobra.Command, args []string) {
|
||||
glog.Flush()
|
||||
|
@ -61,16 +63,18 @@ func NewCIDLCCmd() *cobra.Command {
|
|||
|
||||
cmd.PersistentFlags().BoolVar(
|
||||
&logToStderr, "logtostderr", false, "Log to stderr instead of to files")
|
||||
cmd.PersistentFlags().StringVarP(
|
||||
&name, "name", "n", "", "The Coconut package name")
|
||||
cmd.PersistentFlags().BoolVarP(
|
||||
&recurse, "recurse", "r", false, "Recursively generate code for all sub-packages in the target")
|
||||
cmd.PersistentFlags().StringVar(
|
||||
&outPack, "out-pack", "", "Save generated package metadata to this directory")
|
||||
cmd.PersistentFlags().StringVar(
|
||||
&outRPC, "out-rpc", "", "Save generated RPC provider stubs to this directory")
|
||||
cmd.PersistentFlags().StringVarP(
|
||||
&pkgBase, "pkg-base", "p", "", "Specify the package base where this will be published")
|
||||
cmd.PersistentFlags().StringVarP(
|
||||
&root, "root", "r", "", "Pick a different root directory than `pwd` (the default)")
|
||||
cmd.PersistentFlags().StringVar(
|
||||
&pkgBaseIDL, "pkg-base-idl", "", "Override the base URL where the IDL package is published")
|
||||
cmd.PersistentFlags().StringVar(
|
||||
&pkgBaseRPC, "pkg-base-rpc", "", "Override the base URL where the RPC package is published")
|
||||
cmd.PersistentFlags().BoolVarP(
|
||||
&quiet, "quiet", "q", false, "Suppress non-error output progress messages")
|
||||
cmd.PersistentFlags().IntVarP(
|
||||
&verbose, "verbose", "v", 0, "Enable verbose logging (e.g., v=3); anything >3 is very verbose")
|
||||
|
||||
|
|
|
@ -3,27 +3,84 @@
|
|||
package cidlc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/parser"
|
||||
"go/types"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/tools/go/loader"
|
||||
|
||||
"github.com/pulumi/coconut/pkg/tokens"
|
||||
)
|
||||
|
||||
type CompileOptions struct {
|
||||
Name tokens.PackageName // the package name.
|
||||
Root string // the package root on the filesystem.
|
||||
PkgBase string // the base package name.
|
||||
OutPack string // the package output location.
|
||||
OutRPC string // the RPC output location.
|
||||
Name tokens.PackageName // the package name.
|
||||
PkgBaseIDL string // the base Go package URL for the IDL input.
|
||||
PkgBaseRPC string // the base Go package URL for the RPC output.
|
||||
OutPack string // the package output location.
|
||||
OutRPC string // the RPC output location.
|
||||
Quiet bool // true to suppress innocuous output messages.
|
||||
Recurse bool // true to generate code for all sub-packages.
|
||||
}
|
||||
|
||||
// Compile runs the Go compiler against an IDL project and then generates code for the resulting program.
|
||||
func Compile(opts CompileOptions, paths ...string) error {
|
||||
// First point the Go compiler at the target paths and compile them. Note that this runs both parsing and semantic
|
||||
func Compile(opts CompileOptions, path string) error {
|
||||
// Ensure we are generating *something*.
|
||||
if opts.OutPack == "" && opts.OutRPC == "" {
|
||||
return errors.New("Neither --out-pack nor --out-rpc specified; no code to generate...")
|
||||
}
|
||||
|
||||
// Adjust settings to their defaults and adjust any paths to be absolute.
|
||||
if path == "" {
|
||||
path, _ = os.Getwd()
|
||||
} else {
|
||||
path, _ = filepath.Abs(path)
|
||||
}
|
||||
if opts.PkgBaseIDL == "" {
|
||||
// The default IDL package base is just the GOPATH package path for the target IDL path.
|
||||
if pkgpath, err := goPackagePath(path); err != nil {
|
||||
return err
|
||||
} else {
|
||||
opts.PkgBaseIDL = pkgpath
|
||||
}
|
||||
}
|
||||
if opts.OutPack != "" {
|
||||
opts.OutPack, _ = filepath.Abs(opts.OutPack)
|
||||
}
|
||||
if opts.OutRPC != "" {
|
||||
opts.OutRPC, _ = filepath.Abs(opts.OutRPC)
|
||||
|
||||
// If there is no package base, pick a default based on GOPATH.
|
||||
if opts.PkgBaseRPC == "" {
|
||||
// The default RPC package base, like the IDL package base, defaults to the GOPATH package path.
|
||||
if pkgpath, err := goPackagePath(opts.OutRPC); err != nil {
|
||||
return err
|
||||
} else {
|
||||
opts.PkgBaseRPC = pkgpath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var inputs []string
|
||||
if opts.Recurse {
|
||||
if inp, err := gatherGoPackages(path); err != nil {
|
||||
return err
|
||||
} else {
|
||||
inputs = inp
|
||||
}
|
||||
} else {
|
||||
inputs = []string{opts.PkgBaseIDL}
|
||||
}
|
||||
|
||||
// First point the Go compiler at the target packages to compile. Note that this runs both parsing and semantic
|
||||
// analysis, and will yield an error if anything with the Go program is wrong.
|
||||
var conf loader.Config
|
||||
if _, err := conf.FromArgs(paths, false); err != nil {
|
||||
if _, err := conf.FromArgs(inputs, false); err != nil {
|
||||
return err
|
||||
}
|
||||
conf.ParserMode |= parser.ParseComments // ensure doc comments are retained.
|
||||
|
@ -34,31 +91,50 @@ func Compile(opts CompileOptions, paths ...string) error {
|
|||
|
||||
// Now create in-memory IDL packages, validating contents as we go. The result contains classified elements
|
||||
// such as resources, structs, enum types, and anything required in order to perform subsequent code-generation.
|
||||
chk := NewChecker(opts.Root, prog)
|
||||
chk := NewChecker(path, prog)
|
||||
var packgen *PackGenerator
|
||||
if out := opts.OutPack; out != "" {
|
||||
packgen = NewPackGenerator(opts.Root, out)
|
||||
packgen = NewPackGenerator(prog, path, opts.PkgBaseIDL, out)
|
||||
}
|
||||
var rpcgen *RPCGenerator
|
||||
if out := opts.OutRPC; out != "" {
|
||||
rpcgen = NewRPCGenerator(opts.Root, opts.PkgBase, out)
|
||||
rpcgen = NewRPCGenerator(path, opts.PkgBaseIDL, opts.PkgBaseRPC, out)
|
||||
}
|
||||
for _, pkginfo := range prog.Created {
|
||||
pkg, err := chk.Check(opts.Name, pkginfo)
|
||||
|
||||
// Enumerate all packages (in a deterministic order).
|
||||
var pkgs []*types.Package
|
||||
for pkg := range prog.AllPackages {
|
||||
pkgs = append(pkgs, pkg)
|
||||
}
|
||||
sort.Slice(pkgs, func(i, j int) bool {
|
||||
return pkgs[i].Path() < pkgs[j].Path()
|
||||
})
|
||||
for _, pkg := range pkgs {
|
||||
// Only emit packages that are underneath the base IDL package.
|
||||
if !strings.HasPrefix(pkg.Path(), opts.PkgBaseIDL) {
|
||||
continue
|
||||
}
|
||||
|
||||
pkginfo := prog.AllPackages[pkg]
|
||||
if !opts.Quiet {
|
||||
fmt.Printf("Processing package %v\n", pkginfo.Pkg.Path())
|
||||
}
|
||||
|
||||
outpkg, err := chk.Check(opts.Name, pkginfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Now generate the package output.
|
||||
if packgen != nil {
|
||||
if err = packgen.Generate(pkg); err != nil {
|
||||
if err = packgen.Generate(outpkg); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Next generate the RPC stubs output.
|
||||
if rpcgen != nil {
|
||||
if err = rpcgen.Generate(pkg); err != nil {
|
||||
if err = rpcgen.Generate(outpkg); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -66,3 +142,62 @@ func Compile(opts CompileOptions, paths ...string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// goPackagePath takes a path to a filesystem location and returns its Go package path, based on GOPATH. Given a path
|
||||
// referring to a source location of the form, `$GOPATH/src/...`, the function returns the `...` part.
|
||||
func goPackagePath(path string) (string, error) {
|
||||
// Fetch the GOPATH; it must be set, else we bail out.
|
||||
gopath := os.Getenv("GOPATH")
|
||||
if gopath == "" {
|
||||
return "", errors.New("GOPATH is not set, so package paths cannot be inferred (see --pkg-base-x)")
|
||||
}
|
||||
gopath = filepath.Join(gopath, "src")
|
||||
|
||||
// Now ensure that the package path is a proper subset within it.
|
||||
if !filepath.HasPrefix(path, gopath) {
|
||||
return "", errors.Errorf(
|
||||
"Package root '%v' is not underneath $GOPATH/src, so its package cannot be inferred", path)
|
||||
}
|
||||
|
||||
// Finally, strip off the GOPATH/src prefix, and return the remainder.
|
||||
return path[len(gopath)+1:], nil
|
||||
}
|
||||
|
||||
// gatherGoPackages recurses into a given path and fetches all of its inferred Go packages. The algorithm considers
|
||||
// any sub-directory containing a *.go file, recursively, to be a package. It could, of course, be wrong.
|
||||
func gatherGoPackages(path string) ([]string, error) {
|
||||
var pkgs []string
|
||||
|
||||
// First, if this path contains Go files, append it.
|
||||
var dirs []string
|
||||
hasGoFiles := false
|
||||
if files, err := ioutil.ReadDir(path); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
dirs = append(dirs, file.Name())
|
||||
} else if filepath.Ext(file.Name()) == ".go" {
|
||||
hasGoFiles = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if hasGoFiles {
|
||||
if pkg, err := goPackagePath(path); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
pkgs = append(pkgs, pkg)
|
||||
}
|
||||
}
|
||||
|
||||
// Next, enumerate all directories recursively, to find all Go sub-packages.
|
||||
for _, dir := range dirs {
|
||||
if subpkgs, err := gatherGoPackages(filepath.Join(path, dir)); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
pkgs = append(pkgs, subpkgs...)
|
||||
}
|
||||
}
|
||||
|
||||
return pkgs, nil
|
||||
}
|
||||
|
|
|
@ -10,8 +10,11 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/go/loader"
|
||||
|
||||
"github.com/pulumi/coconut/pkg/tokens"
|
||||
"github.com/pulumi/coconut/pkg/util/contract"
|
||||
)
|
||||
|
@ -19,35 +22,32 @@ import (
|
|||
// TODO: preserve GoDocs.
|
||||
|
||||
type PackGenerator struct {
|
||||
Root string
|
||||
Out string
|
||||
Currpkg *Package // the package currently being visited.
|
||||
Currfile string // the file currently being visited.
|
||||
Fhadres bool // true if the file had at least one resource.
|
||||
Ffimp map[string]string // a map of foreign packages used in a file.
|
||||
Flimp map[tokens.Name]bool // a map of imported members from modules within this package.
|
||||
Program *loader.Program // the compiled Go program.
|
||||
IDLRoot string // the path to the IDL on disk.
|
||||
IDLPkgBase string // the IDL's base package path.
|
||||
Out string // where to store the output package.
|
||||
CurrPkg *Package // the package currently being visited.
|
||||
CurrFile string // the file currently being visited.
|
||||
FileHadRes bool // true if the file had at least one resource.
|
||||
FileImports map[string]MemberImports // a map of imported package paths and members.
|
||||
}
|
||||
|
||||
func NewPackGenerator(root string, out string) *PackGenerator {
|
||||
type MemberImports map[tokens.Name]string
|
||||
|
||||
func NewPackGenerator(prog *loader.Program, root string, pkgBase string, out string) *PackGenerator {
|
||||
return &PackGenerator{
|
||||
Root: root,
|
||||
Out: out,
|
||||
Program: prog,
|
||||
IDLRoot: root,
|
||||
IDLPkgBase: pkgBase,
|
||||
Out: out,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *PackGenerator) Relpath(s string) (string, error) {
|
||||
return filepath.Rel(g.Root, s)
|
||||
}
|
||||
|
||||
// Filename gets the target filename for any given member.
|
||||
func (g *PackGenerator) Filename(pkg *Package, m Member) (string, error) {
|
||||
prog := pkg.Program
|
||||
source := prog.Fset.Position(m.Pos()).Filename // the source filename.`
|
||||
rel, err := g.Relpath(source)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(g.Out, rel), nil
|
||||
// Filename gets the source filename for a given Go element.
|
||||
func (g *PackGenerator) Filename(elem goPos) string {
|
||||
pos := elem.Pos()
|
||||
fset := g.Program.Fset
|
||||
return fset.Position(pos).Filename
|
||||
}
|
||||
|
||||
// Generate generates a Coconut package's source code from a given compiled IDL program.
|
||||
|
@ -58,10 +58,10 @@ func (g *PackGenerator) Generate(pkg *Package) error {
|
|||
}
|
||||
|
||||
// Install context about the current entity being visited.
|
||||
oldpkg, oldfile := g.Currpkg, g.Currfile
|
||||
g.Currpkg = pkg
|
||||
oldpkg, oldfile := g.CurrPkg, g.CurrFile
|
||||
g.CurrPkg = pkg
|
||||
defer (func() {
|
||||
g.Currpkg, g.Currfile = oldpkg, oldfile
|
||||
g.CurrPkg, g.CurrFile = oldpkg, oldfile
|
||||
})()
|
||||
|
||||
// Now walk through the package, file by file, and generate the contents.
|
||||
|
@ -70,7 +70,7 @@ func (g *PackGenerator) Generate(pkg *Package) error {
|
|||
for _, nm := range file.MemberNames {
|
||||
members = append(members, file.Members[nm])
|
||||
}
|
||||
g.Currfile = relpath
|
||||
g.CurrFile = relpath
|
||||
path := filepath.Join(g.Out, relpath)
|
||||
if err := g.EmitFile(path, members); err != nil {
|
||||
return err
|
||||
|
@ -82,12 +82,11 @@ func (g *PackGenerator) Generate(pkg *Package) error {
|
|||
|
||||
func (g *PackGenerator) EmitFile(file string, members []Member) error {
|
||||
// Set up context.
|
||||
oldhadres, oldffimp, oldflimp := g.Fhadres, g.Ffimp, g.Flimp
|
||||
g.Fhadres, g.Ffimp, g.Flimp = false, make(map[string]string), make(map[tokens.Name]bool)
|
||||
oldHadRes, oldImports := g.FileHadRes, g.FileImports
|
||||
g.FileHadRes, g.FileImports = false, make(map[string]MemberImports)
|
||||
defer (func() {
|
||||
g.Fhadres = oldhadres
|
||||
g.Ffimp = oldffimp
|
||||
g.Flimp = oldflimp
|
||||
g.FileHadRes = oldHadRes
|
||||
g.FileImports = oldImports
|
||||
})()
|
||||
|
||||
// First, generate the body. This is required first so we know which imports to emit.
|
||||
|
@ -116,34 +115,68 @@ func (g *PackGenerator) emitFileContents(file string, body string) error {
|
|||
emitHeaderWarning(w)
|
||||
|
||||
// If there are any resources, import the Coconut package.
|
||||
if g.Fhadres {
|
||||
if g.FileHadRes {
|
||||
writefmtln(w, "import * as coconut from \"@coconut/coconut\";")
|
||||
writefmtln(w, "")
|
||||
}
|
||||
if len(g.Flimp) > 0 {
|
||||
for local := range g.Flimp {
|
||||
// For a local import, make sure to manufacture a correct relative import of the members.
|
||||
dir := filepath.Dir(file)
|
||||
module := g.Currpkg.MemberFiles[local].Path
|
||||
relimp, err := filepath.Rel(dir, filepath.Join(g.Out, module))
|
||||
contract.Assert(err == nil)
|
||||
var impname string
|
||||
if strings.HasPrefix(relimp, ".") {
|
||||
impname = relimp
|
||||
} else {
|
||||
impname = "./" + relimp
|
||||
}
|
||||
if filepath.Ext(impname) != "" {
|
||||
lastdot := strings.LastIndex(impname, ".")
|
||||
impname = impname[:lastdot]
|
||||
}
|
||||
writefmtln(w, "import {%v} from \"%v\";", local, impname)
|
||||
if len(g.FileImports) > 0 {
|
||||
// First, sort the imported package names, to ensure determinism.
|
||||
var ipkgs []string
|
||||
for ipkg := range g.FileImports {
|
||||
ipkgs = append(ipkgs, ipkg)
|
||||
}
|
||||
writefmtln(w, "")
|
||||
}
|
||||
if len(g.Ffimp) > 0 {
|
||||
for impname, pkg := range g.Ffimp {
|
||||
contract.Failf("Foreign imports not yet supported: import=%v pkg=%v", impname, pkg)
|
||||
sort.Strings(ipkgs)
|
||||
|
||||
for _, ipkg := range ipkgs {
|
||||
// Produce a map of filename to the members in that file that have been imported.
|
||||
importMap := make(map[string][]tokens.Name)
|
||||
for member, file := range g.FileImports[ipkg] {
|
||||
importMap[file] = append(importMap[file], member)
|
||||
}
|
||||
|
||||
// Now sort them to ensure determinism.
|
||||
var importFiles []string
|
||||
for file := range importMap {
|
||||
importFiles = append(importFiles, file)
|
||||
}
|
||||
sort.Strings(importFiles)
|
||||
|
||||
// Next, walk each imported file and import all members from within it.
|
||||
for _, file := range importFiles {
|
||||
// Make a relative import path from the current file.
|
||||
contract.Assertf(strings.HasPrefix(file, g.IDLRoot),
|
||||
"Inter-IDL package references not yet supported (%v is not part of %v)", file, g.IDLRoot)
|
||||
dir := filepath.Dir(g.CurrFile)
|
||||
relimp, err := filepath.Rel(dir, file[len(g.IDLRoot)+1:])
|
||||
contract.Assertf(err == nil, "Unexpected filepath.Rel error: %v", err)
|
||||
var impname string
|
||||
if strings.HasPrefix(relimp, ".") {
|
||||
impname = relimp
|
||||
} else {
|
||||
impname = "./" + relimp
|
||||
}
|
||||
if filepath.Ext(impname) != "" {
|
||||
lastdot := strings.LastIndex(impname, ".")
|
||||
impname = impname[:lastdot]
|
||||
}
|
||||
|
||||
// Now produce a sorted list of imported members, again to ensure determinism.
|
||||
members := importMap[file]
|
||||
contract.Assert(len(members) > 0)
|
||||
sort.Slice(members, func(i, j int) bool {
|
||||
return string(members[i]) < string(members[j])
|
||||
})
|
||||
|
||||
// Finally, go through and produce the import clause.
|
||||
writefmt(w, "import {")
|
||||
for i, member := range members {
|
||||
if i > 0 {
|
||||
writefmt(w, ", ")
|
||||
}
|
||||
writefmt(w, string(member))
|
||||
}
|
||||
writefmtln(w, "} from \"%v\";", impname)
|
||||
}
|
||||
}
|
||||
writefmtln(w, "")
|
||||
}
|
||||
|
@ -217,7 +250,7 @@ func (g *PackGenerator) EmitResource(w *bufio.Writer, res *Resource) {
|
|||
g.emitStructType(w, res, res.Name()+tokens.Name("Args"))
|
||||
|
||||
// Remember we had a resource in this file so we can import the right stuff.
|
||||
g.Fhadres = true
|
||||
g.FileHadRes = true
|
||||
}
|
||||
|
||||
func (g *PackGenerator) emitResourceClass(w *bufio.Writer, res *Resource) {
|
||||
|
@ -282,19 +315,6 @@ func (g *PackGenerator) emitField(w *bufio.Writer, fld *types.Var, opt PropertyO
|
|||
writefmtln(w, "%v%v%v%v: %v;", prefix, readonly, opt.Name, optional, typ)
|
||||
}
|
||||
|
||||
// registerForeign registers that we have seen a foreign package and requests that the imports be emitted for it.
|
||||
func (g *PackGenerator) registerForeign(pkg *types.Package) string {
|
||||
path := pkg.Path()
|
||||
if impname, has := g.Ffimp[path]; has {
|
||||
return impname
|
||||
}
|
||||
|
||||
// If we haven't seen this yet, allocate an import name for it. For now, just use the package name.
|
||||
name := pkg.Name()
|
||||
g.Ffimp[path] = name
|
||||
return name
|
||||
}
|
||||
|
||||
func (g *PackGenerator) GenTypeName(t types.Type) string {
|
||||
switch u := t.(type) {
|
||||
case *types.Basic:
|
||||
|
@ -311,22 +331,11 @@ func (g *PackGenerator) GenTypeName(t types.Type) string {
|
|||
case *types.Interface:
|
||||
return "any"
|
||||
case *types.Named:
|
||||
// If this came from the same package; the imports will have arranged for it to be available by name.
|
||||
// Our import logic will have arranged for the type name to be available.
|
||||
// TODO: consider auto-generated import names to avoid conflicts between imported and local names.
|
||||
obj := u.Obj()
|
||||
pkg := obj.Pkg()
|
||||
name := obj.Name()
|
||||
if pkg == g.Currpkg.Pkginfo.Pkg {
|
||||
// If this wasn't in the same file, we still need a relative module import to get the name in scope.
|
||||
nm := tokens.Name(name)
|
||||
if g.Currpkg.MemberFiles[nm].Path != g.Currfile {
|
||||
g.Flimp[nm] = true
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// Otherwise, we will need to refer to a qualified import name.
|
||||
impname := g.registerForeign(pkg)
|
||||
return fmt.Sprintf("%v.%v", impname, name)
|
||||
g.trackNameReference(obj)
|
||||
return obj.Name()
|
||||
case *types.Map:
|
||||
return fmt.Sprintf("{[key: %v]: %v}", g.GenTypeName(u.Key()), g.GenTypeName(u.Elem()))
|
||||
case *types.Pointer:
|
||||
|
@ -338,3 +347,24 @@ func (g *PackGenerator) GenTypeName(t types.Type) string {
|
|||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// trackNameReference registers that we have seen a foreign package and requests that the imports be emitted for it.
|
||||
func (g *PackGenerator) trackNameReference(obj *types.TypeName) {
|
||||
// If a reference to a type within the same package and file, there is no need to register anything.
|
||||
pkg := obj.Pkg()
|
||||
member := tokens.Name(obj.Name())
|
||||
if pkg == g.CurrPkg.Pkginfo.Pkg &&
|
||||
g.CurrPkg.MemberFiles[member].Path == g.CurrFile {
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, we need to track the member so that we can import it later on. Make sure not to add duplicates
|
||||
// because we want to ensure we don't import the same thing twice.
|
||||
path := pkg.Path()
|
||||
members, has := g.FileImports[path]
|
||||
if !has {
|
||||
members = make(MemberImports)
|
||||
g.FileImports[path] = members
|
||||
}
|
||||
members[member] = g.Filename(obj)
|
||||
}
|
||||
|
|
|
@ -4,11 +4,13 @@ package cidlc
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/types"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/pulumi/coconut/pkg/tokens"
|
||||
|
@ -16,17 +18,21 @@ import (
|
|||
)
|
||||
|
||||
type RPCGenerator struct {
|
||||
Root string
|
||||
PkgBase string
|
||||
Out string
|
||||
Currpkg *Package // the package currently being visited.
|
||||
IDLRoot string // the root where IDL is loaded from.
|
||||
IDLPkgBase string // the IDL's base package path.
|
||||
RPCPkgBase string // the RPC's base package path.
|
||||
Out string // where RPC stub outputs will be saved.
|
||||
CurrPkg *Package // the package currently being visited.
|
||||
CurrFile string // the file currently being visited.
|
||||
FileImports map[string]string // a map of foreign packages used in a file.
|
||||
}
|
||||
|
||||
func NewRPCGenerator(root string, pkgbase string, out string) *RPCGenerator {
|
||||
func NewRPCGenerator(root, idlPkgBase, rpcPkgBase, out string) *RPCGenerator {
|
||||
return &RPCGenerator{
|
||||
Root: root,
|
||||
PkgBase: pkgbase,
|
||||
Out: out,
|
||||
IDLRoot: root,
|
||||
IDLPkgBase: idlPkgBase,
|
||||
RPCPkgBase: rpcPkgBase,
|
||||
Out: out,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,14 +43,16 @@ func (g *RPCGenerator) Generate(pkg *Package) error {
|
|||
}
|
||||
|
||||
// Install context about the current entity being visited.
|
||||
oldpkg := g.Currpkg
|
||||
g.Currpkg = pkg
|
||||
oldpkg, oldfile := g.CurrPkg, g.CurrFile
|
||||
g.CurrPkg = pkg
|
||||
defer (func() {
|
||||
g.Currpkg = oldpkg
|
||||
g.CurrPkg = oldpkg
|
||||
g.CurrFile = oldfile
|
||||
})()
|
||||
|
||||
// Now walk through the package, file by file, and generate the contents.
|
||||
for relpath, file := range pkg.Files {
|
||||
g.CurrFile = relpath
|
||||
var members []Member
|
||||
for _, nm := range file.MemberNames {
|
||||
members = append(members, file.Members[nm])
|
||||
|
@ -59,6 +67,13 @@ func (g *RPCGenerator) Generate(pkg *Package) error {
|
|||
}
|
||||
|
||||
func (g *RPCGenerator) EmitFile(file string, pkg *Package, members []Member) error {
|
||||
oldimports := g.FileImports
|
||||
g.FileImports = make(map[string]string)
|
||||
defer (func() { g.FileImports = oldimports })()
|
||||
|
||||
// First, generate the body. This is required first so we know which imports to emit.
|
||||
body := g.genFileBody(file, pkg, members)
|
||||
|
||||
// Open up a writer that overwrites whatever file contents already exist.
|
||||
f, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
|
@ -78,17 +93,55 @@ func (g *RPCGenerator) EmitFile(file string, pkg *Package, members []Member) err
|
|||
// TODO: what about imports for cross-package references.
|
||||
writefmtln(w, "import (")
|
||||
writefmtln(w, ` "errors"`)
|
||||
writefmtln(w, "")
|
||||
writefmtln(w, ` pbempty "github.com/golang/protobuf/ptypes/empty"`)
|
||||
writefmtln(w, ` pbstruct "github.com/golang/protobuf/ptypes/struct"`)
|
||||
writefmtln(w, ` "golang.org/x/net/context"`)
|
||||
writefmtln(w, "")
|
||||
writefmtln(w, ` "github.com/pulumi/coconut/pkg/resource"`)
|
||||
writefmtln(w, ` "github.com/pulumi/coconut/pkg/tokens"`)
|
||||
writefmtln(w, ` "github.com/pulumi/coconut/pkg/util/contract"`)
|
||||
writefmtln(w, ` "github.com/pulumi/coconut/pkg/util/mapper"`)
|
||||
writefmtln(w, ` "github.com/pulumi/coconut/sdk/go/pkg/cocorpc"`)
|
||||
writefmtln(w, ` "golang.org/x/net/context"`)
|
||||
if len(g.FileImports) > 0 {
|
||||
writefmtln(w, "")
|
||||
// Sort the imports so they are in a correct, deterministic order.
|
||||
var imports []string
|
||||
for imp := range g.FileImports {
|
||||
imports = append(imports, imp)
|
||||
}
|
||||
sort.Strings(imports)
|
||||
|
||||
// Now just emit a list of imports with their given names.
|
||||
for _, imp := range imports {
|
||||
name := g.FileImports[imp]
|
||||
|
||||
// If the import referenced one of the IDL packages, we must rewrite it to an RPC package.
|
||||
contract.Assertf(strings.HasPrefix(imp, g.IDLPkgBase),
|
||||
"Inter-IDL package references not yet supported (%v is not part of %v)", imp, g.IDLPkgBase)
|
||||
var imppath string
|
||||
if imp == g.IDLPkgBase {
|
||||
imppath = g.RPCPkgBase
|
||||
} else {
|
||||
relimp := imp[len(g.IDLPkgBase)+1:]
|
||||
imppath = g.RPCPkgBase + "/" + relimp
|
||||
}
|
||||
|
||||
writefmtln(w, ` %v "%v"`, name, imppath)
|
||||
}
|
||||
}
|
||||
writefmtln(w, ")")
|
||||
writefmtln(w, "")
|
||||
|
||||
// Now finally emit the actual body and close out the file.
|
||||
writefmtln(w, "%v", body)
|
||||
return w.Flush()
|
||||
}
|
||||
|
||||
func (g *RPCGenerator) genFileBody(file string, pkg *Package, members []Member) string {
|
||||
var buffer bytes.Buffer
|
||||
w := bufio.NewWriter(&buffer)
|
||||
|
||||
// First, for each RPC struct/resource member, emit its appropriate generated code.
|
||||
var typedefs []Typedef
|
||||
var consts []*Const
|
||||
|
@ -121,7 +174,8 @@ func (g *RPCGenerator) EmitFile(file string, pkg *Package, members []Member) err
|
|||
g.EmitConstants(w, consts)
|
||||
}
|
||||
|
||||
return w.Flush()
|
||||
w.Flush()
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
// getFileModule generates a module name from a filename. To do so, we simply find the path part after the root and
|
||||
|
@ -413,13 +467,13 @@ func (g *RPCGenerator) GenTypeName(t types.Type) string {
|
|||
name := obj.Name()
|
||||
|
||||
// If this came from the same package, Go can access it without qualification.
|
||||
if pkg == g.Currpkg.Pkginfo.Pkg {
|
||||
if pkg == g.CurrPkg.Pkginfo.Pkg {
|
||||
return name
|
||||
}
|
||||
|
||||
// Otherwise, we will need to refer to a qualified import name.
|
||||
// TODO: we will need to generate the right imports before we can emit such names.
|
||||
contract.Failf("Cross-package IDL references not yet supported: pkg=%v name=%v", pkg, name)
|
||||
impname := g.registerImport(pkg)
|
||||
return fmt.Sprintf("%v.%v", impname, name)
|
||||
case *types.Map:
|
||||
return fmt.Sprintf("map[%v]%v", g.GenTypeName(u.Key()), g.GenTypeName(u.Elem()))
|
||||
case *types.Pointer:
|
||||
|
@ -432,6 +486,20 @@ func (g *RPCGenerator) GenTypeName(t types.Type) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// registerImport registers that we have seen a foreign package and requests that the imports be emitted for it.
|
||||
func (g *RPCGenerator) registerImport(pkg *types.Package) string {
|
||||
path := pkg.Path()
|
||||
if impname, has := g.FileImports[path]; has {
|
||||
return impname
|
||||
}
|
||||
|
||||
// If we haven't seen this yet, allocate an import name for it. For now, we just use the package name with two
|
||||
// leading underscores, to avoid accidental collisions with other names in the file.
|
||||
name := "__" + pkg.Name()
|
||||
g.FileImports[path] = name
|
||||
return name
|
||||
}
|
||||
|
||||
func (g *RPCGenerator) EmitTypedefs(w *bufio.Writer, typedefs []Typedef) {
|
||||
writefmtln(w, "/* Typedefs */")
|
||||
writefmtln(w, "")
|
||||
|
|
|
@ -15,16 +15,16 @@ import (
|
|||
)
|
||||
|
||||
type Package struct {
|
||||
Name tokens.PackageName // the name of the package.
|
||||
Name tokens.PackageName // the package name.
|
||||
Program *loader.Program // the fully parsed/analyzed Go program.
|
||||
Pkginfo *loader.PackageInfo // the Go package information.
|
||||
Files map[string]*File // the files inside of this package.
|
||||
MemberFiles map[tokens.Name]*File // a map from member to the file containing it.
|
||||
}
|
||||
|
||||
func NewPackage(nm tokens.PackageName, prog *loader.Program, pkginfo *loader.PackageInfo) *Package {
|
||||
func NewPackage(name tokens.PackageName, prog *loader.Program, pkginfo *loader.PackageInfo) *Package {
|
||||
return &Package{
|
||||
Name: nm,
|
||||
Name: name,
|
||||
Program: prog,
|
||||
Pkginfo: pkginfo,
|
||||
Files: make(map[string]*File),
|
||||
|
|
Loading…
Reference in a new issue