github.com/lenfree/buffalo@v0.7.3-0.20170207163156-891616ea4064/buffalo/cmd/build.go (about) 1 package cmd 2 3 import ( 4 "archive/zip" 5 "bytes" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "regexp" 13 "runtime" 14 "strings" 15 "time" 16 17 "github.com/gobuffalo/buffalo/buffalo/cmd/generate" 18 "github.com/gobuffalo/velvet" 19 "github.com/spf13/cobra" 20 ) 21 22 var outputBinName string 23 var zipBin bool 24 var extractAssets bool 25 26 type builder struct { 27 cleanup []string 28 originalMain []byte 29 originalApp []byte 30 workDir string 31 } 32 33 func (b *builder) clean(name ...string) string { 34 path := filepath.Join(name...) 35 b.cleanup = append(b.cleanup, path) 36 return path 37 } 38 39 func (b *builder) exec(name string, args ...string) error { 40 cmd := exec.Command(name, args...) 41 fmt.Printf("--> running %s\n", strings.Join(cmd.Args, " ")) 42 cmd.Stdin = os.Stdin 43 cmd.Stderr = os.Stderr 44 cmd.Stdout = os.Stdout 45 return cmd.Run() 46 } 47 48 func (b *builder) execQuiet(name string, args ...string) error { 49 cmd := exec.Command(name, args...) 50 return cmd.Run() 51 } 52 53 func (b *builder) buildWebpack() error { 54 _, err := os.Stat("webpack.config.js") 55 if err == nil { 56 // build webpack 57 return b.exec(generate.WebpackPath) 58 } 59 return nil 60 } 61 62 func (b *builder) buildAPack() error { 63 err := os.MkdirAll(b.clean("a"), 0766) 64 if err != nil { 65 return err 66 } 67 err = b.buildAInit() 68 if err != nil { 69 return err 70 } 71 err = b.buildDatabase() 72 if err != nil { 73 return err 74 } 75 return nil 76 } 77 78 func (b *builder) buildAInit() error { 79 a, err := os.Create(b.clean("a", "a.go")) 80 if err != nil { 81 return err 82 } 83 a.WriteString(aGo) 84 return nil 85 } 86 87 func (b *builder) buildDatabase() error { 88 bb := &bytes.Buffer{} 89 dgo, err := os.Create(b.clean("a", "database.go")) 90 if err != nil { 91 return err 92 } 93 _, err = os.Stat("database.yml") 94 if err == nil { 95 // copy the database.yml file to the migrations folder so it's available through rice 96 os.MkdirAll("./migrations", 0755) 97 d, err := os.Open("database.yml") 98 if err != nil { 99 return err 100 } 101 _, err = io.Copy(bb, d) 102 if err != nil { 103 return err 104 } 105 } 106 dgo.WriteString("package a\n") 107 dgo.WriteString(fmt.Sprintf("var DB_CONFIG = `%s`", bb.String())) 108 return nil 109 } 110 111 func (b *builder) buildRiceZip() error { 112 defer os.Chdir(b.workDir) 113 _, err := exec.LookPath("rice") 114 if err == nil { 115 paths := map[string]bool{} 116 // if rice exists, try and build some cleanup: 117 err = filepath.Walk(b.workDir, func(path string, info os.FileInfo, err error) error { 118 if info.IsDir() { 119 base := filepath.Base(path) 120 if base == "node_modules" || base == ".git" || base == "bin" { 121 return filepath.SkipDir 122 } 123 } else { 124 err = os.Chdir(filepath.Dir(path)) 125 if err != nil { 126 return err 127 } 128 129 s, err := ioutil.ReadFile(path) 130 if err != nil { 131 return err 132 } 133 rx := regexp.MustCompile("(rice.FindBox|rice.MustFindBox)") 134 if rx.Match(s) && filepath.Ext(info.Name()) == ".go" { 135 gopath := strings.Replace(filepath.Join(os.Getenv("GOPATH"), "src"), "\\", "/", -1) 136 pkg := strings.Replace(filepath.Dir(strings.Replace(path, gopath+"/", "", -1)), "\\", "/", -1) 137 paths[pkg] = true 138 } 139 } 140 return nil 141 }) 142 if err != nil { 143 return err 144 } 145 if len(paths) != 0 { 146 args := []string{"append", "--exec", filepath.Join(b.workDir, outputBinName)} 147 for k := range paths { 148 args = append(args, "-i", k) 149 } 150 return b.exec("rice", args...) 151 } 152 // rice append --exec example 153 } 154 return nil 155 } 156 func (b *builder) buildRiceEmbedded() error { 157 defer os.Chdir(b.workDir) 158 _, err := exec.LookPath("rice") 159 if err == nil { 160 // if rice exists, try and build some cleanup: 161 err = filepath.Walk(b.workDir, func(path string, info os.FileInfo, err error) error { 162 if info.IsDir() { 163 base := filepath.Base(path) 164 if base == "node_modules" || base == ".git" { 165 return filepath.SkipDir 166 } 167 err = os.Chdir(path) 168 if err != nil { 169 return err 170 } 171 err = b.execQuiet("rice", "embed-go") 172 if err == nil { 173 bp := filepath.Join(path, "rice-box.go") 174 _, err := os.Stat(bp) 175 if err == nil { 176 fmt.Printf("--> built rice box %s\n", bp) 177 b.clean(bp) 178 } 179 } 180 } 181 return nil 182 }) 183 if err != nil { 184 return err 185 } 186 // rice append --exec example 187 } 188 return nil 189 } 190 191 func (b *builder) disableAssetsHandling() error { 192 defer os.Chdir(b.workDir) 193 fmt.Printf("--> disable self assets handling\n") 194 195 newApp := strings.Replace(string(b.originalApp), "app.ServeFiles(\"/assets\", assetsPath())", "//app.ServeFiles(\"/assets\", assetsPath())", 1) 196 197 appgo, err := os.Create("actions/app.go") 198 if err != nil { 199 return err 200 } 201 _, err = appgo.WriteString(newApp) 202 if err != nil { 203 return err 204 } 205 206 return nil 207 } 208 209 func (b *builder) buildAssetsArchive() error { 210 defer os.Chdir(b.workDir) 211 fmt.Printf("--> build assets archive\n") 212 213 outputDir := filepath.Dir(outputBinName) 214 assetsName := filepath.Base(outputBinName) 215 target := outputDir + "/" + assetsName + "-assets.zip" 216 source := filepath.Join(b.workDir, "public", "assets") 217 218 zipfile, err := os.Create(target) 219 if err != nil { 220 return err 221 } 222 defer zipfile.Close() 223 224 archive := zip.NewWriter(zipfile) 225 defer archive.Close() 226 227 info, err := os.Stat(source) 228 if err != nil { 229 return err 230 } 231 232 var baseDir string 233 if info.IsDir() { 234 baseDir = filepath.Base(source) 235 } 236 237 filepath.Walk(source, func(path string, info os.FileInfo, err error) error { 238 if err != nil { 239 return err 240 } 241 242 header, err := zip.FileInfoHeader(info) 243 if err != nil { 244 return err 245 } 246 247 if baseDir != "" { 248 header.Name = filepath.Join(baseDir, strings.TrimPrefix(path, source)) 249 } 250 251 if info.IsDir() { 252 header.Name += "/" 253 } else { 254 header.Method = zip.Deflate 255 } 256 257 writer, err := archive.CreateHeader(header) 258 if err != nil { 259 return err 260 } 261 262 if info.IsDir() { 263 return nil 264 } 265 266 file, err := os.Open(path) 267 if err != nil { 268 return err 269 } 270 defer file.Close() 271 _, err = io.Copy(writer, file) 272 return err 273 }) 274 275 return err 276 } 277 278 func (b *builder) buildMain() error { 279 newMain := strings.Replace(string(b.originalMain), "func main()", "func originalMain()", 1) 280 maingo, err := os.Create("main.go") 281 if err != nil { 282 return err 283 } 284 _, err = maingo.WriteString(newMain) 285 if err != nil { 286 return err 287 } 288 289 ctx := velvet.NewContext() 290 ctx.Set("root", rootPath) 291 ctx.Set("modelsPack", packagePath(rootPath)+"/models") 292 ctx.Set("aPack", packagePath(rootPath)+"/a") 293 ctx.Set("name", filepath.Base(rootPath)) 294 s, err := velvet.Render(buildMainTmpl, ctx) 295 if err != nil { 296 return err 297 } 298 f, err := os.Create(b.clean("buffalo_build_main.go")) 299 if err != nil { 300 return err 301 } 302 f.WriteString(s) 303 304 return nil 305 } 306 307 func (b *builder) cleanupBuild() { 308 fmt.Println("--> cleaning up build") 309 for _, b := range b.cleanup { 310 fmt.Printf("----> cleaning up %s\n", b) 311 os.RemoveAll(b) 312 } 313 maingo, _ := os.Create("main.go") 314 maingo.Write(b.originalMain) 315 316 appgo, _ := os.Create("actions/app.go") 317 appgo.Write(b.originalApp) 318 } 319 320 func (b *builder) cleanupTarget() { 321 fmt.Println("--> cleaning up target dir") 322 323 // Create output directory if not exists 324 outputDir := filepath.Dir(outputBinName) 325 326 if _, err := os.Stat(outputDir); os.IsNotExist(err) { 327 os.MkdirAll(outputDir, 0776) 328 fmt.Printf("----> creating target dir %s\n", outputDir) 329 } 330 331 files, _ := ioutil.ReadDir(outputDir) 332 for _, f := range files { 333 fmt.Printf("----> cleaning up %s\n", f.Name()) 334 os.RemoveAll(outputDir + f.Name()) 335 } 336 } 337 338 func (b *builder) run() error { 339 err := b.buildMain() 340 if err != nil { 341 return err 342 } 343 344 err = b.buildWebpack() 345 if err != nil { 346 return err 347 } 348 349 err = b.buildAPack() 350 if err != nil { 351 return err 352 } 353 354 err = b.buildMain() 355 if err != nil { 356 return err 357 } 358 359 if extractAssets { 360 err = b.buildAssetsArchive() 361 if err != nil { 362 return err 363 } 364 err = b.disableAssetsHandling() 365 if err != nil { 366 return err 367 } 368 return b.buildBin() 369 } 370 371 if zipBin { 372 err = b.buildBin() 373 if err != nil { 374 return err 375 } 376 return b.buildRiceZip() 377 } 378 379 err = b.buildRiceEmbedded() 380 if err != nil { 381 return err 382 } 383 return b.buildBin() 384 } 385 386 func (b *builder) buildBin() error { 387 buildArgs := []string{"build", "-v", "-o", outputBinName} 388 _, err := exec.LookPath("git") 389 buildTime := fmt.Sprintf("\"%s\"", time.Now().Format(time.RFC3339)) 390 version := buildTime 391 if err == nil { 392 cmd := exec.Command("git", "rev-parse", "--short", "HEAD") 393 out := &bytes.Buffer{} 394 cmd.Stdout = out 395 err = cmd.Run() 396 if err == nil && out.String() != "" { 397 version = strings.TrimSpace(out.String()) 398 } 399 } 400 buildArgs = append(buildArgs, "-ldflags", fmt.Sprintf("-X main.version=%s -X main.buildTime=%s", version, buildTime)) 401 402 return b.exec("go", buildArgs...) 403 } 404 405 // buildCmd represents the build command 406 var buildCmd = &cobra.Command{ 407 Use: "build", 408 Aliases: []string{"b", "bill"}, 409 Short: "Builds a Buffalo binary, including bundling of assets (go.rice & webpack)", 410 RunE: func(cc *cobra.Command, args []string) error { 411 originalMain := &bytes.Buffer{} 412 maingo, err := os.Open("main.go") 413 _, err = originalMain.ReadFrom(maingo) 414 if err != nil { 415 return err 416 } 417 maingo.Close() 418 419 originalApp := &bytes.Buffer{} 420 appgo, err := os.Open("actions/app.go") 421 _, err = originalApp.ReadFrom(appgo) 422 if err != nil { 423 return err 424 } 425 appgo.Close() 426 427 pwd, _ := os.Getwd() 428 b := builder{ 429 cleanup: []string{}, 430 originalMain: originalMain.Bytes(), 431 originalApp: originalApp.Bytes(), 432 workDir: pwd, 433 } 434 defer b.cleanupBuild() 435 436 b.cleanupTarget() 437 return b.run() 438 }, 439 } 440 441 func init() { 442 RootCmd.AddCommand(buildCmd) 443 pwd, _ := os.Getwd() 444 output := filepath.Join("bin", filepath.Base(pwd)) 445 446 if runtime.GOOS == "windows" { 447 output += ".exe" 448 } 449 450 buildCmd.Flags().StringVarP(&outputBinName, "output", "o", output, "set the name of the binary") 451 buildCmd.Flags().BoolVarP(&zipBin, "zip", "z", false, "zips the assets to the binary, this requires zip installed") 452 buildCmd.Flags().BoolVarP(&extractAssets, "extract-assets", "e", false, "extract the assets and put them in a distinct archive") 453 } 454 455 var buildMainTmpl = `package main 456 457 import ( 458 "fmt" 459 "io/ioutil" 460 "log" 461 "os" 462 "path/filepath" 463 464 rice "github.com/GeertJohan/go.rice" 465 _ "{{aPack}}" 466 "{{modelsPack}}" 467 ) 468 469 var version = "unknown" 470 var buildTime = "unknown" 471 var migrationBox *rice.Box 472 473 func main() { 474 args := os.Args 475 if len(args) == 1 { 476 originalMain() 477 } 478 c := args[1] 479 switch c { 480 case "migrate": 481 migrate() 482 case "start", "run", "serve": 483 printVersion() 484 originalMain() 485 case "version": 486 printVersion() 487 default: 488 log.Fatalf("Could not find a command named: %s", c) 489 } 490 } 491 492 func printVersion() { 493 fmt.Printf("{{name}} version %s (%s)\n\n", version, buildTime) 494 } 495 496 func migrate() { 497 var err error 498 migrationBox, err = rice.FindBox("./migrations") 499 if err != nil { 500 fmt.Println("--> No migrations found.") 501 return 502 } 503 fmt.Println("--> Running migrations") 504 path, err := unpackMigrations() 505 if err != nil { 506 log.Fatalf("Failed to unpack migrations: %s", err) 507 } 508 defer os.RemoveAll(path) 509 510 models.DB.MigrateUp(path) 511 } 512 513 func unpackMigrations() (string, error) { 514 dir, err := ioutil.TempDir("", "{{name}}-migrations") 515 if err != nil { 516 log.Fatalf("Unable to create temp directory: %s", err) 517 } 518 519 migrationBox.Walk("", func(path string, fi os.FileInfo, e error) error { 520 if !fi.IsDir() { 521 content := migrationBox.MustBytes(path) 522 file := filepath.Join(dir, path) 523 if err := ioutil.WriteFile(file, content, 0666); err != nil { 524 log.Fatalf("Failed to write migration to disk: %s", err) 525 } 526 } 527 return e 528 }) 529 530 return dir, nil 531 }` 532 533 var aGo = `package a 534 535 import ( 536 "log" 537 "os" 538 ) 539 540 func init() { 541 dropDatabaseYml() 542 } 543 544 func dropDatabaseYml() { 545 if DB_CONFIG != "" { 546 547 _, err := os.Stat("database.yml") 548 if err == nil { 549 // yaml already exists, don't do anything 550 return 551 } 552 f, err := os.Create("database.yml") 553 if err != nil { 554 log.Fatal(err) 555 } 556 _, err = f.WriteString(DB_CONFIG) 557 if err != nil { 558 log.Fatal(err) 559 } 560 } 561 }`