github.com/brycereitano/goa@v0.0.0-20170315073847-8ffa6c85e265/goagen/codegen/workspace.go (about)

     1  package codegen
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"go/ast"
     7  	"go/build"
     8  	"go/format"
     9  	"go/parser"
    10  	"go/scanner"
    11  	"go/token"
    12  	"io/ioutil"
    13  	"os"
    14  	"os/exec"
    15  	"path/filepath"
    16  	"runtime"
    17  	"strings"
    18  	"text/template"
    19  
    20  	"github.com/goadesign/goa/version"
    21  
    22  	"golang.org/x/tools/go/ast/astutil"
    23  )
    24  
    25  type (
    26  	// Workspace represents a temporary Go workspace
    27  	Workspace struct {
    28  		// Path is the absolute path to the workspace directory.
    29  		Path string
    30  		// gopath is the original GOPATH
    31  		gopath string
    32  	}
    33  
    34  	// Package represents a temporary Go package
    35  	Package struct {
    36  		// (Go) Path of package
    37  		Path string
    38  		// Workspace containing package
    39  		Workspace *Workspace
    40  	}
    41  
    42  	// SourceFile represents a single Go source file
    43  	SourceFile struct {
    44  		// Name of the source file
    45  		Name string
    46  		// Package containing source file
    47  		Package *Package
    48  	}
    49  )
    50  
    51  var (
    52  	// Template used to render Go source file headers.
    53  	headerTmpl = template.Must(template.New("header").Funcs(DefaultFuncMap).Parse(headerT))
    54  
    55  	// DefaultFuncMap is the FuncMap used to initialize all source file templates.
    56  	DefaultFuncMap = template.FuncMap{
    57  		"add":                 func(a, b int) int { return a + b },
    58  		"commandLine":         CommandLine,
    59  		"comment":             Comment,
    60  		"goify":               Goify,
    61  		"goifyatt":            GoifyAtt,
    62  		"gonative":            GoNativeType,
    63  		"gotypedef":           GoTypeDef,
    64  		"gotypename":          GoTypeName,
    65  		"gotypedesc":          GoTypeDesc,
    66  		"gotyperef":           GoTypeRef,
    67  		"join":                strings.Join,
    68  		"recursivePublicizer": RecursivePublicizer,
    69  		"tabs":                Tabs,
    70  		"tempvar":             Tempvar,
    71  		"title":               strings.Title,
    72  		"toLower":             strings.ToLower,
    73  		"validationChecker":   ValidationChecker,
    74  	}
    75  )
    76  
    77  // NewWorkspace returns a newly created temporary Go workspace.
    78  // Use Delete to delete the corresponding temporary directory when done.
    79  func NewWorkspace(prefix string) (*Workspace, error) {
    80  	dir, err := ioutil.TempDir("", prefix)
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  	// create workspace layout
    85  	os.MkdirAll(filepath.Join(dir, "src"), 0755)
    86  	os.MkdirAll(filepath.Join(dir, "pkg"), 0755)
    87  	os.MkdirAll(filepath.Join(dir, "bin"), 0755)
    88  
    89  	// setup GOPATH
    90  	gopath := os.Getenv("GOPATH")
    91  	os.Setenv("GOPATH", fmt.Sprintf("%s%c%s", dir, os.PathListSeparator, gopath))
    92  
    93  	// we're done
    94  	return &Workspace{Path: dir, gopath: gopath}, nil
    95  }
    96  
    97  // WorkspaceFor returns the Go workspace for the given Go source file.
    98  func WorkspaceFor(source string) (*Workspace, error) {
    99  	gopaths := os.Getenv("GOPATH")
   100  	// We use absolute paths so that in particular on Windows the case gets normalized
   101  	sourcePath, err := filepath.Abs(source)
   102  	if err != nil {
   103  		sourcePath = source
   104  	}
   105  	for _, gp := range filepath.SplitList(gopaths) {
   106  		gopath, err := filepath.Abs(gp)
   107  		if err != nil {
   108  			gopath = gp
   109  		}
   110  		if filepath.HasPrefix(sourcePath, gopath) {
   111  			return &Workspace{
   112  				gopath: gopaths,
   113  				Path:   gopath,
   114  			}, nil
   115  		}
   116  	}
   117  	return nil, fmt.Errorf(`Go source file "%s" not in Go workspace, adjust GOPATH %s`, source, gopaths)
   118  }
   119  
   120  // Delete deletes the workspace temporary directory.
   121  func (w *Workspace) Delete() {
   122  	if w.gopath != "" {
   123  		os.Setenv("GOPATH", w.gopath)
   124  	}
   125  	os.RemoveAll(w.Path)
   126  }
   127  
   128  // Reset removes all content from the workspace.
   129  func (w *Workspace) Reset() error {
   130  	d, err := os.Open(w.Path)
   131  	if err != nil {
   132  		return err
   133  	}
   134  	defer d.Close()
   135  	names, err := d.Readdirnames(-1)
   136  	if err != nil {
   137  		return err
   138  	}
   139  	for _, name := range names {
   140  		err = os.RemoveAll(filepath.Join(w.Path, name))
   141  		if err != nil {
   142  			return err
   143  		}
   144  	}
   145  	return nil
   146  }
   147  
   148  // NewPackage creates a new package in the workspace. It deletes any pre-existing package.
   149  // goPath is the go package path used to import the package.
   150  func (w *Workspace) NewPackage(goPath string) (*Package, error) {
   151  	pkg := &Package{Path: goPath, Workspace: w}
   152  	os.RemoveAll(pkg.Abs())
   153  	if err := os.MkdirAll(pkg.Abs(), 0755); err != nil {
   154  		return nil, err
   155  	}
   156  	return pkg, nil
   157  }
   158  
   159  // PackageFor returns the package for the given source file.
   160  func PackageFor(source string) (*Package, error) {
   161  	w, err := WorkspaceFor(source)
   162  	if err != nil {
   163  		return nil, err
   164  	}
   165  	path, err := filepath.Rel(filepath.Join(w.Path, "src"), filepath.Dir(source))
   166  	if err != nil {
   167  		return nil, err
   168  	}
   169  	return &Package{Workspace: w, Path: path}, nil
   170  }
   171  
   172  // Abs returns the absolute path to the package source directory
   173  func (p *Package) Abs() string {
   174  	return filepath.Join(p.Workspace.Path, "src", p.Path)
   175  }
   176  
   177  // CreateSourceFile creates a Go source file in the given package.
   178  func (p *Package) CreateSourceFile(name string) *SourceFile {
   179  	path := filepath.Join(p.Abs(), name)
   180  	os.Remove(filepath.Join(path, name))
   181  	return &SourceFile{Name: name, Package: p}
   182  }
   183  
   184  // Compile compiles a package and returns the path to the compiled binary.
   185  func (p *Package) Compile(bin string) (string, error) {
   186  	gobin, err := exec.LookPath("go")
   187  	if err != nil {
   188  		return "", fmt.Errorf(`failed to find a go compiler, looked in "%s"`, os.Getenv("PATH"))
   189  	}
   190  	if runtime.GOOS == "windows" {
   191  		bin += ".exe"
   192  	}
   193  	c := exec.Cmd{
   194  		Path: gobin,
   195  		Args: []string{gobin, "build", "-o", bin},
   196  		Dir:  p.Abs(),
   197  	}
   198  	out, err := c.CombinedOutput()
   199  	if err != nil {
   200  		if len(out) > 0 {
   201  			return "", fmt.Errorf(string(out))
   202  		}
   203  		return "", fmt.Errorf("failed to compile %s: %s", bin, err)
   204  	}
   205  	return filepath.Join(p.Abs(), bin), nil
   206  }
   207  
   208  // SourceFileFor returns a SourceFile for the file at the given path.
   209  func SourceFileFor(path string) (*SourceFile, error) {
   210  	absPath, err := filepath.Abs(path)
   211  	if err != nil {
   212  		absPath = path
   213  	}
   214  	p, err := PackageFor(absPath)
   215  	if err != nil {
   216  		return nil, err
   217  	}
   218  	return &SourceFile{
   219  		Package: p,
   220  		Name:    filepath.Base(absPath),
   221  	}, nil
   222  }
   223  
   224  // WriteHeader writes the generic generated code header.
   225  func (f *SourceFile) WriteHeader(title, pack string, imports []*ImportSpec) error {
   226  	ctx := map[string]interface{}{
   227  		"Title":       title,
   228  		"ToolVersion": version.String(),
   229  		"Pkg":         pack,
   230  		"Imports":     imports,
   231  	}
   232  	if err := headerTmpl.Execute(f, ctx); err != nil {
   233  		return fmt.Errorf("failed to generate contexts: %s", err)
   234  	}
   235  	return nil
   236  }
   237  
   238  // Write implements io.Writer so that variables of type *SourceFile can be
   239  // used in template.Execute.
   240  func (f *SourceFile) Write(b []byte) (int, error) {
   241  	file, err := os.OpenFile(f.Abs(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
   242  	if err != nil {
   243  		return 0, err
   244  	}
   245  	defer file.Close()
   246  	return file.Write(b)
   247  }
   248  
   249  // FormatCode runs "goimports -w" on the source file.
   250  func (f *SourceFile) FormatCode() error {
   251  	// Parse file into AST
   252  	fset := token.NewFileSet()
   253  	file, err := parser.ParseFile(fset, f.Abs(), nil, parser.ParseComments)
   254  	if err != nil {
   255  		content, _ := ioutil.ReadFile(f.Abs())
   256  		var buf bytes.Buffer
   257  		scanner.PrintError(&buf, err)
   258  		return fmt.Errorf("%s\n========\nContent:\n%s", buf.String(), content)
   259  	}
   260  	// Clean unused imports
   261  	imports := astutil.Imports(fset, file)
   262  	for _, group := range imports {
   263  		for _, imp := range group {
   264  			path := strings.Trim(imp.Path.Value, `"`)
   265  			if !astutil.UsesImport(file, path) {
   266  				if imp.Name != nil {
   267  					astutil.DeleteNamedImport(fset, file, imp.Name.Name, path)
   268  				} else {
   269  					astutil.DeleteImport(fset, file, path)
   270  				}
   271  			}
   272  		}
   273  	}
   274  	ast.SortImports(fset, file)
   275  	// Open file to be written
   276  	w, err := os.OpenFile(f.Abs(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm)
   277  	if err != nil {
   278  		return err
   279  	}
   280  	defer w.Close()
   281  	// Write formatted code without unused imports
   282  	return format.Node(w, fset, file)
   283  }
   284  
   285  // Abs returne the source file absolute filename
   286  func (f *SourceFile) Abs() string {
   287  	return filepath.Join(f.Package.Abs(), f.Name)
   288  }
   289  
   290  // ExecuteTemplate executes the template and writes the output to the file.
   291  func (f *SourceFile) ExecuteTemplate(name, source string, funcMap template.FuncMap, data interface{}) error {
   292  	tmpl, err := template.New(name).Funcs(DefaultFuncMap).Funcs(funcMap).Parse(source)
   293  	if err != nil {
   294  		panic(err) // bug
   295  	}
   296  	return tmpl.Execute(f, data)
   297  }
   298  
   299  // PackagePath returns the Go package path for the directory that lives under the given absolute
   300  // file path.
   301  func PackagePath(path string) (string, error) {
   302  	absPath, err := filepath.Abs(path)
   303  	if err != nil {
   304  		absPath = path
   305  	}
   306  	gopaths := filepath.SplitList(os.Getenv("GOPATH"))
   307  	for _, gopath := range gopaths {
   308  		if gp, err := filepath.Abs(gopath); err == nil {
   309  			gopath = gp
   310  		}
   311  		if filepath.HasPrefix(absPath, gopath) {
   312  			base := filepath.FromSlash(gopath + "/src")
   313  			rel, err := filepath.Rel(base, absPath)
   314  			return filepath.ToSlash(rel), err
   315  		}
   316  	}
   317  	return "", fmt.Errorf("%s does not contain a Go package", absPath)
   318  }
   319  
   320  // PackageSourcePath returns the absolute path to the given package source.
   321  func PackageSourcePath(pkg string) (string, error) {
   322  	buildCtx := build.Default
   323  	buildCtx.GOPATH = os.Getenv("GOPATH") // Reevaluate each time to be nice to tests
   324  	wd, err := os.Getwd()
   325  	if err != nil {
   326  		wd = "."
   327  	}
   328  	p, err := buildCtx.Import(pkg, wd, 0)
   329  	if err != nil {
   330  		return "", err
   331  	}
   332  	return p.Dir, nil
   333  }
   334  
   335  // PackageName returns the name of a package at the given path
   336  func PackageName(path string) (string, error) {
   337  	fset := token.NewFileSet()
   338  	pkgs, err := parser.ParseDir(fset, path, nil, parser.PackageClauseOnly)
   339  	if err != nil {
   340  		return "", err
   341  	}
   342  	var pkgNames []string
   343  	for n := range pkgs {
   344  		if !strings.HasSuffix(n, "_test") {
   345  			pkgNames = append(pkgNames, n)
   346  		}
   347  	}
   348  	if len(pkgNames) > 1 {
   349  		return "", fmt.Errorf("more than one Go package found in %s (%s)",
   350  			path, strings.Join(pkgNames, ","))
   351  	}
   352  	if len(pkgNames) == 0 {
   353  		return "", fmt.Errorf("no Go package found in %s", path)
   354  	}
   355  	return pkgNames[0], nil
   356  }
   357  
   358  const (
   359  	headerT = `{{if .Title}}// Code generated by goagen {{.ToolVersion}}, command line:
   360  {{comment commandLine}}
   361  //
   362  // {{.Title}}
   363  //
   364  // The content of this file is auto-generated, DO NOT MODIFY
   365  
   366  {{end}}package {{.Pkg}}
   367  
   368  {{if .Imports}}import {{if gt (len .Imports) 1}}(
   369  {{end}}{{range .Imports}}	{{.Code}}
   370  {{end}}{{if gt (len .Imports) 1}})
   371  {{end}}
   372  {{end}}`
   373  )