github.com/brycereitano/goa@v0.0.0-20170315073847-8ffa6c85e265/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 } 49 ) 50 51 var ( 52 // Template used to render Go source file headers. 53 headerTmpl = template.Must(template.New("header").Funcs(DefaultFuncMap).Parse(headerT)) 54 55 // DefaultFuncMap is the FuncMap used to initialize all source file templates. 56 DefaultFuncMap = template.FuncMap{ 57 "add": func(a, b int) int { return a + b }, 58 "commandLine": CommandLine, 59 "comment": Comment, 60 "goify": Goify, 61 "goifyatt": GoifyAtt, 62 "gonative": GoNativeType, 63 "gotypedef": GoTypeDef, 64 "gotypename": GoTypeName, 65 "gotypedesc": GoTypeDesc, 66 "gotyperef": GoTypeRef, 67 "join": strings.Join, 68 "recursivePublicizer": RecursivePublicizer, 69 "tabs": Tabs, 70 "tempvar": Tempvar, 71 "title": strings.Title, 72 "toLower": strings.ToLower, 73 "validationChecker": ValidationChecker, 74 } 75 ) 76 77 // NewWorkspace returns a newly created temporary Go workspace. 78 // Use Delete to delete the corresponding temporary directory when done. 79 func NewWorkspace(prefix string) (*Workspace, error) { 80 dir, err := ioutil.TempDir("", prefix) 81 if err != nil { 82 return nil, err 83 } 84 // create workspace layout 85 os.MkdirAll(filepath.Join(dir, "src"), 0755) 86 os.MkdirAll(filepath.Join(dir, "pkg"), 0755) 87 os.MkdirAll(filepath.Join(dir, "bin"), 0755) 88 89 // setup GOPATH 90 gopath := os.Getenv("GOPATH") 91 os.Setenv("GOPATH", fmt.Sprintf("%s%c%s", dir, os.PathListSeparator, gopath)) 92 93 // we're done 94 return &Workspace{Path: dir, gopath: gopath}, nil 95 } 96 97 // WorkspaceFor returns the Go workspace for the given Go source file. 98 func WorkspaceFor(source string) (*Workspace, error) { 99 gopaths := os.Getenv("GOPATH") 100 // We use absolute paths so that in particular on Windows the case gets normalized 101 sourcePath, err := filepath.Abs(source) 102 if err != nil { 103 sourcePath = source 104 } 105 for _, gp := range filepath.SplitList(gopaths) { 106 gopath, err := filepath.Abs(gp) 107 if err != nil { 108 gopath = gp 109 } 110 if filepath.HasPrefix(sourcePath, gopath) { 111 return &Workspace{ 112 gopath: gopaths, 113 Path: gopath, 114 }, nil 115 } 116 } 117 return nil, fmt.Errorf(`Go source file "%s" not in Go workspace, adjust GOPATH %s`, source, gopaths) 118 } 119 120 // Delete deletes the workspace temporary directory. 121 func (w *Workspace) Delete() { 122 if w.gopath != "" { 123 os.Setenv("GOPATH", w.gopath) 124 } 125 os.RemoveAll(w.Path) 126 } 127 128 // Reset removes all content from the workspace. 129 func (w *Workspace) Reset() error { 130 d, err := os.Open(w.Path) 131 if err != nil { 132 return err 133 } 134 defer d.Close() 135 names, err := d.Readdirnames(-1) 136 if err != nil { 137 return err 138 } 139 for _, name := range names { 140 err = os.RemoveAll(filepath.Join(w.Path, name)) 141 if err != nil { 142 return err 143 } 144 } 145 return nil 146 } 147 148 // NewPackage creates a new package in the workspace. It deletes any pre-existing package. 149 // goPath is the go package path used to import the package. 150 func (w *Workspace) NewPackage(goPath string) (*Package, error) { 151 pkg := &Package{Path: goPath, Workspace: w} 152 os.RemoveAll(pkg.Abs()) 153 if err := os.MkdirAll(pkg.Abs(), 0755); err != nil { 154 return nil, err 155 } 156 return pkg, nil 157 } 158 159 // PackageFor returns the package for the given source file. 160 func PackageFor(source string) (*Package, error) { 161 w, err := WorkspaceFor(source) 162 if err != nil { 163 return nil, err 164 } 165 path, err := filepath.Rel(filepath.Join(w.Path, "src"), filepath.Dir(source)) 166 if err != nil { 167 return nil, err 168 } 169 return &Package{Workspace: w, Path: path}, nil 170 } 171 172 // Abs returns the absolute path to the package source directory 173 func (p *Package) Abs() string { 174 return filepath.Join(p.Workspace.Path, "src", p.Path) 175 } 176 177 // CreateSourceFile creates a Go source file in the given package. 178 func (p *Package) CreateSourceFile(name string) *SourceFile { 179 path := filepath.Join(p.Abs(), name) 180 os.Remove(filepath.Join(path, name)) 181 return &SourceFile{Name: name, Package: p} 182 } 183 184 // Compile compiles a package and returns the path to the compiled binary. 185 func (p *Package) Compile(bin string) (string, error) { 186 gobin, err := exec.LookPath("go") 187 if err != nil { 188 return "", fmt.Errorf(`failed to find a go compiler, looked in "%s"`, os.Getenv("PATH")) 189 } 190 if runtime.GOOS == "windows" { 191 bin += ".exe" 192 } 193 c := exec.Cmd{ 194 Path: gobin, 195 Args: []string{gobin, "build", "-o", bin}, 196 Dir: p.Abs(), 197 } 198 out, err := c.CombinedOutput() 199 if err != nil { 200 if len(out) > 0 { 201 return "", fmt.Errorf(string(out)) 202 } 203 return "", fmt.Errorf("failed to compile %s: %s", bin, err) 204 } 205 return filepath.Join(p.Abs(), bin), nil 206 } 207 208 // SourceFileFor returns a SourceFile for the file at the given path. 209 func SourceFileFor(path string) (*SourceFile, error) { 210 absPath, err := filepath.Abs(path) 211 if err != nil { 212 absPath = path 213 } 214 p, err := PackageFor(absPath) 215 if err != nil { 216 return nil, err 217 } 218 return &SourceFile{ 219 Package: p, 220 Name: filepath.Base(absPath), 221 }, nil 222 } 223 224 // WriteHeader writes the generic generated code header. 225 func (f *SourceFile) WriteHeader(title, pack string, imports []*ImportSpec) error { 226 ctx := map[string]interface{}{ 227 "Title": title, 228 "ToolVersion": version.String(), 229 "Pkg": pack, 230 "Imports": imports, 231 } 232 if err := headerTmpl.Execute(f, ctx); err != nil { 233 return fmt.Errorf("failed to generate contexts: %s", err) 234 } 235 return nil 236 } 237 238 // Write implements io.Writer so that variables of type *SourceFile can be 239 // used in template.Execute. 240 func (f *SourceFile) Write(b []byte) (int, error) { 241 file, err := os.OpenFile(f.Abs(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) 242 if err != nil { 243 return 0, err 244 } 245 defer file.Close() 246 return file.Write(b) 247 } 248 249 // FormatCode runs "goimports -w" on the source file. 250 func (f *SourceFile) FormatCode() error { 251 // Parse file into AST 252 fset := token.NewFileSet() 253 file, err := parser.ParseFile(fset, f.Abs(), nil, parser.ParseComments) 254 if err != nil { 255 content, _ := ioutil.ReadFile(f.Abs()) 256 var buf bytes.Buffer 257 scanner.PrintError(&buf, err) 258 return fmt.Errorf("%s\n========\nContent:\n%s", buf.String(), content) 259 } 260 // Clean unused imports 261 imports := astutil.Imports(fset, file) 262 for _, group := range imports { 263 for _, imp := range group { 264 path := strings.Trim(imp.Path.Value, `"`) 265 if !astutil.UsesImport(file, path) { 266 if imp.Name != nil { 267 astutil.DeleteNamedImport(fset, file, imp.Name.Name, path) 268 } else { 269 astutil.DeleteImport(fset, file, path) 270 } 271 } 272 } 273 } 274 ast.SortImports(fset, file) 275 // Open file to be written 276 w, err := os.OpenFile(f.Abs(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm) 277 if err != nil { 278 return err 279 } 280 defer w.Close() 281 // Write formatted code without unused imports 282 return format.Node(w, fset, file) 283 } 284 285 // Abs returne the source file absolute filename 286 func (f *SourceFile) Abs() string { 287 return filepath.Join(f.Package.Abs(), f.Name) 288 } 289 290 // ExecuteTemplate executes the template and writes the output to the file. 291 func (f *SourceFile) ExecuteTemplate(name, source string, funcMap template.FuncMap, data interface{}) error { 292 tmpl, err := template.New(name).Funcs(DefaultFuncMap).Funcs(funcMap).Parse(source) 293 if err != nil { 294 panic(err) // bug 295 } 296 return tmpl.Execute(f, data) 297 } 298 299 // PackagePath returns the Go package path for the directory that lives under the given absolute 300 // file path. 301 func PackagePath(path string) (string, error) { 302 absPath, err := filepath.Abs(path) 303 if err != nil { 304 absPath = path 305 } 306 gopaths := filepath.SplitList(os.Getenv("GOPATH")) 307 for _, gopath := range gopaths { 308 if gp, err := filepath.Abs(gopath); err == nil { 309 gopath = gp 310 } 311 if filepath.HasPrefix(absPath, gopath) { 312 base := filepath.FromSlash(gopath + "/src") 313 rel, err := filepath.Rel(base, absPath) 314 return filepath.ToSlash(rel), err 315 } 316 } 317 return "", fmt.Errorf("%s does not contain a Go package", absPath) 318 } 319 320 // PackageSourcePath returns the absolute path to the given package source. 321 func PackageSourcePath(pkg string) (string, error) { 322 buildCtx := build.Default 323 buildCtx.GOPATH = os.Getenv("GOPATH") // Reevaluate each time to be nice to tests 324 wd, err := os.Getwd() 325 if err != nil { 326 wd = "." 327 } 328 p, err := buildCtx.Import(pkg, wd, 0) 329 if err != nil { 330 return "", err 331 } 332 return p.Dir, nil 333 } 334 335 // PackageName returns the name of a package at the given path 336 func PackageName(path string) (string, error) { 337 fset := token.NewFileSet() 338 pkgs, err := parser.ParseDir(fset, path, nil, parser.PackageClauseOnly) 339 if err != nil { 340 return "", err 341 } 342 var pkgNames []string 343 for n := range pkgs { 344 if !strings.HasSuffix(n, "_test") { 345 pkgNames = append(pkgNames, n) 346 } 347 } 348 if len(pkgNames) > 1 { 349 return "", fmt.Errorf("more than one Go package found in %s (%s)", 350 path, strings.Join(pkgNames, ",")) 351 } 352 if len(pkgNames) == 0 { 353 return "", fmt.Errorf("no Go package found in %s", path) 354 } 355 return pkgNames[0], nil 356 } 357 358 const ( 359 headerT = `{{if .Title}}// Code generated by goagen {{.ToolVersion}}, command line: 360 {{comment commandLine}} 361 // 362 // {{.Title}} 363 // 364 // The content of this file is auto-generated, DO NOT MODIFY 365 366 {{end}}package {{.Pkg}} 367 368 {{if .Imports}}import {{if gt (len .Imports) 1}}( 369 {{end}}{{range .Imports}} {{.Code}} 370 {{end}}{{if gt (len .Imports) 1}}) 371 {{end}} 372 {{end}}` 373 )