github.com/ManabuSeki/goa-v1@v1.4.3/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  	"strconv"
    18  	"strings"
    19  	"text/template"
    20  
    21  	"github.com/goadesign/goa/version"
    22  
    23  	"golang.org/x/tools/go/ast/astutil"
    24  )
    25  
    26  type (
    27  	// Workspace represents a temporary Go workspace
    28  	Workspace struct {
    29  		// Path is the absolute path to the workspace directory.
    30  		Path string
    31  		// gopath is the original GOPATH
    32  		gopath string
    33  		// isModuleMode indicates whether the Module mode is enabled.
    34  		isModuleMode bool
    35  	}
    36  
    37  	// Package represents a temporary Go package
    38  	Package struct {
    39  		// (Go) Path of package
    40  		Path string
    41  		// Workspace containing package
    42  		Workspace *Workspace
    43  	}
    44  
    45  	// SourceFile represents a single Go source file
    46  	SourceFile struct {
    47  		// Name of the source file
    48  		Name string
    49  		// Package containing source file
    50  		Package *Package
    51  		// osFile is the underlying OS file.
    52  		osFile *os.File
    53  	}
    54  )
    55  
    56  var (
    57  	// Template used to render Go source file headers.
    58  	headerTmpl = template.Must(template.New("header").Funcs(DefaultFuncMap).Parse(headerT))
    59  
    60  	// DefaultFuncMap is the FuncMap used to initialize all source file templates.
    61  	DefaultFuncMap = template.FuncMap{
    62  		"add":                 func(a, b int) int { return a + b },
    63  		"commandLine":         CommandLine,
    64  		"comment":             Comment,
    65  		"goify":               Goify,
    66  		"goifyatt":            GoifyAtt,
    67  		"gonative":            GoNativeType,
    68  		"gotypedef":           GoTypeDef,
    69  		"gotypename":          GoTypeName,
    70  		"gotypedesc":          GoTypeDesc,
    71  		"gotyperef":           GoTypeRef,
    72  		"join":                strings.Join,
    73  		"recursivePublicizer": RecursivePublicizer,
    74  		"tabs":                Tabs,
    75  		"tempvar":             Tempvar,
    76  		"title":               strings.Title,
    77  		"toLower":             strings.ToLower,
    78  		"validationChecker":   ValidationChecker,
    79  	}
    80  )
    81  
    82  // NewWorkspace returns a newly created temporary Go workspace.
    83  // Use Delete to delete the corresponding temporary directory when done.
    84  func NewWorkspace(prefix string) (*Workspace, error) {
    85  	dir, err := ioutil.TempDir("", prefix)
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  	// create workspace layout
    90  	os.MkdirAll(filepath.Join(dir, "src"), 0755)
    91  	os.MkdirAll(filepath.Join(dir, "pkg"), 0755)
    92  	os.MkdirAll(filepath.Join(dir, "bin"), 0755)
    93  
    94  	// setup GOPATH
    95  	gopath := envOr("GOPATH", build.Default.GOPATH)
    96  	os.Setenv("GOPATH", fmt.Sprintf("%s%c%s", dir, os.PathListSeparator, gopath))
    97  
    98  	// we're done
    99  	return &Workspace{Path: dir, gopath: gopath}, nil
   100  }
   101  
   102  // WorkspaceFor returns the Go workspace for the given Go source file.
   103  func WorkspaceFor(source string) (*Workspace, error) {
   104  	gopaths := envOr("GOPATH", build.Default.GOPATH)
   105  	// We use absolute paths so that in particular on Windows the case gets normalized
   106  	sourcePath, err := filepath.Abs(source)
   107  	if err != nil {
   108  		sourcePath = source
   109  	}
   110  	if os.Getenv("GO111MODULE") != "on" { // GOPATH mode
   111  		for _, gp := range filepath.SplitList(gopaths) {
   112  			gopath, err := filepath.Abs(gp)
   113  			if err != nil {
   114  				gopath = gp
   115  			}
   116  			if filepath.HasPrefix(sourcePath, gopath) {
   117  				return &Workspace{
   118  					gopath:       gopaths,
   119  					isModuleMode: false,
   120  					Path:         gopath,
   121  				}, nil
   122  			}
   123  		}
   124  	}
   125  	if os.Getenv("GO111MODULE") != "off" { // Module mode
   126  		root, _ := findModuleRoot(sourcePath, "", false)
   127  		if root != "" {
   128  			return &Workspace{
   129  				gopath:       gopaths,
   130  				isModuleMode: true,
   131  				Path:         root,
   132  			}, nil
   133  		}
   134  	}
   135  	return nil, fmt.Errorf(`Go source file "%s" not in Go workspace, adjust GOPATH %s or use modules`, source, gopaths)
   136  }
   137  
   138  // Delete deletes the workspace temporary directory.
   139  func (w *Workspace) Delete() {
   140  	if w.gopath != "" {
   141  		os.Setenv("GOPATH", w.gopath)
   142  	}
   143  	os.RemoveAll(w.Path)
   144  }
   145  
   146  // Reset removes all content from the workspace.
   147  func (w *Workspace) Reset() error {
   148  	d, err := os.Open(w.Path)
   149  	if err != nil {
   150  		return err
   151  	}
   152  	defer d.Close()
   153  	names, err := d.Readdirnames(-1)
   154  	if err != nil {
   155  		return err
   156  	}
   157  	for _, name := range names {
   158  		err = os.RemoveAll(filepath.Join(w.Path, name))
   159  		if err != nil {
   160  			return err
   161  		}
   162  	}
   163  	return nil
   164  }
   165  
   166  // NewPackage creates a new package in the workspace. It deletes any pre-existing package.
   167  // goPath is the go package path used to import the package.
   168  func (w *Workspace) NewPackage(goPath string) (*Package, error) {
   169  	pkg := &Package{Path: goPath, Workspace: w}
   170  	os.RemoveAll(pkg.Abs())
   171  	if err := os.MkdirAll(pkg.Abs(), 0755); err != nil {
   172  		return nil, err
   173  	}
   174  	return pkg, nil
   175  }
   176  
   177  // PackageFor returns the package for the given source file.
   178  func PackageFor(source string) (*Package, error) {
   179  	w, err := WorkspaceFor(source)
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  	basepath := filepath.Join(w.Path, "src") // GOPATH mode.
   184  	if w.isModuleMode {
   185  		basepath = w.Path // Module mode.
   186  	}
   187  	path, err := filepath.Rel(basepath, filepath.Dir(source))
   188  	if err != nil {
   189  		return nil, err
   190  	}
   191  	return &Package{Workspace: w, Path: filepath.ToSlash(path)}, nil
   192  }
   193  
   194  // Abs returns the absolute path to the package source directory
   195  func (p *Package) Abs() string {
   196  	elem := "src" // GOPATH mode.
   197  	if p.Workspace.isModuleMode {
   198  		elem = "" // Module mode.
   199  	}
   200  	return filepath.Join(p.Workspace.Path, elem, p.Path)
   201  }
   202  
   203  // CreateSourceFile creates a Go source file in the given package. If the file
   204  // already exists it is overwritten.
   205  func (p *Package) CreateSourceFile(name string) (*SourceFile, error) {
   206  	os.RemoveAll(filepath.Join(p.Abs(), name))
   207  	return p.OpenSourceFile(name)
   208  }
   209  
   210  // OpenSourceFile opens an existing file to append to it. If the file does not
   211  // exist OpenSourceFile creates it.
   212  func (p *Package) OpenSourceFile(name string) (*SourceFile, error) {
   213  	f := &SourceFile{Name: name, Package: p}
   214  	file, err := os.OpenFile(f.Abs(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
   215  	if err != nil {
   216  		return nil, err
   217  	}
   218  	f.osFile = file
   219  	return f, nil
   220  }
   221  
   222  // Compile compiles a package and returns the path to the compiled binary.
   223  func (p *Package) Compile(bin string) (string, error) {
   224  	gobin, err := exec.LookPath("go")
   225  	if err != nil {
   226  		return "", fmt.Errorf(`failed to find a go compiler, looked in "%s"`, os.Getenv("PATH"))
   227  	}
   228  	if runtime.GOOS == "windows" {
   229  		bin += ".exe"
   230  	}
   231  	c := exec.Cmd{
   232  		Path: gobin,
   233  		Args: []string{gobin, "build", "-o", bin},
   234  		Dir:  p.Abs(),
   235  	}
   236  	out, err := c.CombinedOutput()
   237  	if err != nil {
   238  		if len(out) > 0 {
   239  			return "", fmt.Errorf(string(out))
   240  		}
   241  		return "", fmt.Errorf("failed to compile %s: %s", bin, err)
   242  	}
   243  	return filepath.Join(p.Abs(), bin), nil
   244  }
   245  
   246  // SourceFileFor returns a SourceFile for the file at the given path.
   247  func SourceFileFor(path string) (*SourceFile, error) {
   248  	absPath, err := filepath.Abs(path)
   249  	if err != nil {
   250  		absPath = path
   251  	}
   252  	p, err := PackageFor(absPath)
   253  	if err != nil {
   254  		return nil, err
   255  	}
   256  	return p.OpenSourceFile(filepath.Base(absPath))
   257  }
   258  
   259  // WriteHeader writes the generic generated code header.
   260  func (f *SourceFile) WriteHeader(title, pack string, imports []*ImportSpec) error {
   261  	ctx := map[string]interface{}{
   262  		"Title":       title,
   263  		"ToolVersion": version.String(),
   264  		"Pkg":         pack,
   265  		"Imports":     imports,
   266  	}
   267  	if err := headerTmpl.Execute(f, ctx); err != nil {
   268  		return fmt.Errorf("failed to generate contexts: %s", err)
   269  	}
   270  	return nil
   271  }
   272  
   273  // Write implements io.Writer so that variables of type *SourceFile can be
   274  // used in template.Execute.
   275  func (f *SourceFile) Write(b []byte) (int, error) {
   276  	return f.osFile.Write(b)
   277  }
   278  
   279  // Close closes the underlying OS file.
   280  func (f *SourceFile) Close() {
   281  	if err := f.osFile.Close(); err != nil {
   282  		panic(err) // bug
   283  	}
   284  }
   285  
   286  // FormatCode performs the equivalent of "goimports -w" on the source file.
   287  func (f *SourceFile) FormatCode() error {
   288  	// Parse file into AST
   289  	fset := token.NewFileSet()
   290  	file, err := parser.ParseFile(fset, f.Abs(), nil, parser.ParseComments)
   291  	if err != nil {
   292  		content, _ := ioutil.ReadFile(f.Abs())
   293  		var buf bytes.Buffer
   294  		scanner.PrintError(&buf, err)
   295  		return fmt.Errorf("%s\n========\nContent:\n%s", buf.String(), content)
   296  	}
   297  	// Clean unused imports
   298  	imports := astutil.Imports(fset, file)
   299  	for _, group := range imports {
   300  		for _, imp := range group {
   301  			path := strings.Trim(imp.Path.Value, `"`)
   302  			if !astutil.UsesImport(file, path) {
   303  				if imp.Name != nil {
   304  					astutil.DeleteNamedImport(fset, file, imp.Name.Name, path)
   305  				} else {
   306  					astutil.DeleteImport(fset, file, path)
   307  				}
   308  			}
   309  		}
   310  	}
   311  	ast.SortImports(fset, file)
   312  	// Open file to be written
   313  	w, err := os.OpenFile(f.Abs(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm)
   314  	if err != nil {
   315  		return err
   316  	}
   317  	defer w.Close()
   318  	// Write formatted code without unused imports
   319  	return format.Node(w, fset, file)
   320  }
   321  
   322  // Abs returne the source file absolute filename
   323  func (f *SourceFile) Abs() string {
   324  	return filepath.Join(f.Package.Abs(), f.Name)
   325  }
   326  
   327  // ExecuteTemplate executes the template and writes the output to the file.
   328  func (f *SourceFile) ExecuteTemplate(name, source string, funcMap template.FuncMap, data interface{}) error {
   329  	tmpl, err := template.New(name).Funcs(DefaultFuncMap).Funcs(funcMap).Parse(source)
   330  	if err != nil {
   331  		panic(err) // bug
   332  	}
   333  	return tmpl.Execute(f, data)
   334  }
   335  
   336  // PackagePath returns the Go package path for the directory that lives under the given absolute
   337  // file path.
   338  func PackagePath(path string) (string, error) {
   339  	absPath, err := filepath.Abs(path)
   340  	if err != nil {
   341  		absPath = path
   342  	}
   343  	gopaths := filepath.SplitList(envOr("GOPATH", build.Default.GOPATH))
   344  	if os.Getenv("GO111MODULE") != "on" { // GOPATH mode
   345  		for _, gopath := range gopaths {
   346  			if gp, err := filepath.Abs(gopath); err == nil {
   347  				gopath = gp
   348  			}
   349  			if filepath.HasPrefix(absPath, gopath) {
   350  				base := filepath.FromSlash(gopath + "/src")
   351  				rel, err := filepath.Rel(base, absPath)
   352  				return filepath.ToSlash(rel), err
   353  			}
   354  		}
   355  	}
   356  	if os.Getenv("GO111MODULE") != "off" { // Module mode
   357  		root, file := findModuleRoot(absPath, "", false)
   358  		if root != "" {
   359  			content, err := ioutil.ReadFile(filepath.Join(root, file))
   360  			if err == nil {
   361  				p := modulePath(content)
   362  				base := filepath.FromSlash(root)
   363  				rel, err := filepath.Rel(base, absPath)
   364  				return filepath.ToSlash(filepath.Join(p, rel)), err
   365  			}
   366  		}
   367  	}
   368  	return "", fmt.Errorf("%s does not contain a Go package", absPath)
   369  }
   370  
   371  // PackageSourcePath returns the absolute path to the given package source.
   372  func PackageSourcePath(pkg string) (string, error) {
   373  	buildCtx := build.Default
   374  	buildCtx.GOPATH = envOr("GOPATH", build.Default.GOPATH) // Reevaluate each time to be nice to tests
   375  	wd, err := os.Getwd()
   376  	if err != nil {
   377  		wd = "."
   378  	}
   379  	p, err := buildCtx.Import(pkg, wd, 0)
   380  	if err != nil {
   381  		return "", err
   382  	}
   383  	return p.Dir, nil
   384  }
   385  
   386  // PackageName returns the name of a package at the given path
   387  func PackageName(path string) (string, error) {
   388  	fset := token.NewFileSet()
   389  	pkgs, err := parser.ParseDir(fset, path, nil, parser.PackageClauseOnly)
   390  	if err != nil {
   391  		return "", err
   392  	}
   393  	var pkgNames []string
   394  	for n := range pkgs {
   395  		if !strings.HasSuffix(n, "_test") {
   396  			pkgNames = append(pkgNames, n)
   397  		}
   398  	}
   399  	if len(pkgNames) > 1 {
   400  		return "", fmt.Errorf("more than one Go package found in %s (%s)",
   401  			path, strings.Join(pkgNames, ","))
   402  	}
   403  	if len(pkgNames) == 0 {
   404  		return "", fmt.Errorf("no Go package found in %s", path)
   405  	}
   406  	return pkgNames[0], nil
   407  }
   408  
   409  // Copied from cmd/go/internal/modload/init.go.
   410  var altConfigs = []string{
   411  	"Gopkg.lock",
   412  
   413  	"GLOCKFILE",
   414  	"Godeps/Godeps.json",
   415  	"dependencies.tsv",
   416  	"glide.lock",
   417  	"vendor.conf",
   418  	"vendor.yml",
   419  	"vendor/manifest",
   420  	"vendor/vendor.json",
   421  
   422  	".git/config",
   423  }
   424  
   425  // Copied from cmd/go/internal/modload/init.go.
   426  func findModuleRoot(dir, limit string, legacyConfigOK bool) (root, file string) {
   427  	dir = filepath.Clean(dir)
   428  	dir1 := dir
   429  	limit = filepath.Clean(limit)
   430  
   431  	// Look for enclosing go.mod.
   432  	for {
   433  		if fi, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil && !fi.IsDir() {
   434  			return dir, "go.mod"
   435  		}
   436  		if dir == limit {
   437  			break
   438  		}
   439  		d := filepath.Dir(dir)
   440  		if d == dir {
   441  			break
   442  		}
   443  		dir = d
   444  	}
   445  
   446  	// Failing that, look for enclosing alternate version config.
   447  	if legacyConfigOK {
   448  		dir = dir1
   449  		for {
   450  			for _, name := range altConfigs {
   451  				if fi, err := os.Stat(filepath.Join(dir, name)); err == nil && !fi.IsDir() {
   452  					return dir, name
   453  				}
   454  			}
   455  			if dir == limit {
   456  				break
   457  			}
   458  			d := filepath.Dir(dir)
   459  			if d == dir {
   460  				break
   461  			}
   462  			dir = d
   463  		}
   464  	}
   465  
   466  	return "", ""
   467  }
   468  
   469  // Copied from cmd/go/internal/modfile/read.go
   470  var (
   471  	slashSlash = []byte("//")
   472  	moduleStr  = []byte("module")
   473  )
   474  
   475  // Copied from cmd/go/internal/modfile/read.go
   476  func modulePath(mod []byte) string {
   477  	for len(mod) > 0 {
   478  		line := mod
   479  		mod = nil
   480  		if i := bytes.IndexByte(line, '\n'); i >= 0 {
   481  			line, mod = line[:i], line[i+1:]
   482  		}
   483  		if i := bytes.Index(line, slashSlash); i >= 0 {
   484  			line = line[:i]
   485  		}
   486  		line = bytes.TrimSpace(line)
   487  		if !bytes.HasPrefix(line, moduleStr) {
   488  			continue
   489  		}
   490  		line = line[len(moduleStr):]
   491  		n := len(line)
   492  		line = bytes.TrimSpace(line)
   493  		if len(line) == n || len(line) == 0 {
   494  			continue
   495  		}
   496  
   497  		if line[0] == '"' || line[0] == '`' {
   498  			p, err := strconv.Unquote(string(line))
   499  			if err != nil {
   500  				return "" // malformed quoted string or multiline module path
   501  			}
   502  			return p
   503  		}
   504  
   505  		return string(line)
   506  	}
   507  	return "" // missing module path
   508  }
   509  
   510  const (
   511  	headerT = `{{if .Title}}// Code generated by goagen {{.ToolVersion}}, DO NOT EDIT.
   512  //
   513  // {{.Title}}
   514  //
   515  // Command:
   516  {{comment commandLine}}
   517  
   518  {{end}}package {{.Pkg}}
   519  
   520  {{if .Imports}}import {{if gt (len .Imports) 1}}(
   521  {{end}}{{range .Imports}}	{{.Code}}
   522  {{end}}{{if gt (len .Imports) 1}})
   523  {{end}}
   524  {{end}}`
   525  )