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 )