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