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 )