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  }`