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 )