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