github.com/mayra-cabrera/buffalo@v0.9.4-0.20170814145312-66d2e7772f11/buffalo/cmd/build.go (about)

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