203 lines
5.7 KiB
Go
203 lines
5.7 KiB
Go
// Copyright 2017 Pulumi, Inc. All rights reserved.
|
|
|
|
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.
|
|
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.
|
|
Recursive 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, 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.
|
|
pkgpath, err := goPackagePath(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
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.
|
|
pkgpath, err := goPackagePath(opts.OutRPC)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
opts.PkgBaseRPC = pkgpath
|
|
}
|
|
}
|
|
|
|
var inputs []string
|
|
if opts.Recursive {
|
|
inp, err := gatherGoPackages(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
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(inputs, false); err != nil {
|
|
return err
|
|
}
|
|
conf.ParserMode |= parser.ParseComments // ensure doc comments are retained.
|
|
prog, err := conf.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 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(path, prog)
|
|
var packgen *PackGenerator
|
|
if out := opts.OutPack; out != "" {
|
|
packgen = NewPackGenerator(prog, path, opts.PkgBaseIDL, out)
|
|
}
|
|
var rpcgen *RPCGenerator
|
|
if out := opts.OutRPC; out != "" {
|
|
rpcgen = NewRPCGenerator(path, opts.PkgBaseIDL, opts.PkgBaseRPC, out)
|
|
}
|
|
|
|
// 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(outpkg); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Next generate the RPC stubs output.
|
|
if rpcgen != nil {
|
|
if err = rpcgen.Generate(outpkg); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
files, err := ioutil.ReadDir(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, file := range files {
|
|
if file.IsDir() {
|
|
dirs = append(dirs, file.Name())
|
|
} else if filepath.Ext(file.Name()) == ".go" {
|
|
hasGoFiles = true
|
|
}
|
|
}
|
|
if hasGoFiles {
|
|
pkg, err := goPackagePath(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pkgs = append(pkgs, pkg)
|
|
}
|
|
|
|
// Next, enumerate all directories recursively, to find all Go sub-packages.
|
|
for _, dir := range dirs {
|
|
subpkgs, err := gatherGoPackages(filepath.Join(path, dir))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pkgs = append(pkgs, subpkgs...)
|
|
}
|
|
|
|
return pkgs, nil
|
|
}
|