github.com/naoina/kocha@v0.7.1-0.20171129072645-78c7a531f799/cmd/kocha-build/main.go (about)

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"go/build"
     6  	"io/ioutil"
     7  	"os"
     8  	"os/exec"
     9  	"path"
    10  	"path/filepath"
    11  	"runtime"
    12  	"strings"
    13  	"text/template"
    14  	"time"
    15  
    16  	"github.com/naoina/kocha"
    17  	"github.com/naoina/kocha/util"
    18  )
    19  
    20  type buildCommand struct {
    21  	option struct {
    22  		All  bool   `short:"a" long:"all"`
    23  		Tag  string `short:"t" long:"tag"`
    24  		Help bool   `short:"h" long:"help"`
    25  	}
    26  }
    27  
    28  func (c *buildCommand) Name() string {
    29  	return "kocha build"
    30  }
    31  
    32  func (c *buildCommand) Usage() string {
    33  	return fmt.Sprintf(`Usage: %s [OPTIONS] [IMPORT_PATH]
    34  
    35  Build your application.
    36  
    37  Options:
    38      -a, --all         make the true all-in-one binary
    39      -t, --tag=TAG     specify version tag
    40      -h, --help        display this help and exit
    41  
    42  `, c.Name())
    43  }
    44  
    45  func (c *buildCommand) Option() interface{} {
    46  	return &c.option
    47  }
    48  
    49  func (c *buildCommand) Run(args []string) (err error) {
    50  	var appDir string
    51  	var dir string
    52  	if len(args) > 0 {
    53  		appDir = args[0]
    54  		dir, err = util.FindAbsDir(appDir)
    55  		if err != nil {
    56  			return err
    57  		}
    58  	} else {
    59  		dir, err = os.Getwd()
    60  		if err != nil {
    61  			return err
    62  		}
    63  		appDir, err = util.FindAppDir()
    64  		if err != nil {
    65  			return err
    66  		}
    67  	}
    68  	appName := filepath.Base(dir)
    69  	configPkg, err := getPackage(path.Join(appDir, "config"))
    70  	if err != nil {
    71  		return fmt.Errorf(`cannot import "%s": %v`, path.Join(appDir, "config"), err)
    72  	}
    73  	var dbImportPath string
    74  	if dbPkg, err := getPackage(path.Join(appDir, "db")); err == nil {
    75  		dbImportPath = dbPkg.ImportPath
    76  	}
    77  	var migrationImportPath string
    78  	if migrationPkg, err := getPackage(path.Join(appDir, "db", "migration")); err == nil {
    79  		migrationImportPath = migrationPkg.ImportPath
    80  	}
    81  	tmpDir, err := filepath.Abs("tmp")
    82  	if err != nil {
    83  		return err
    84  	}
    85  	if err := os.Mkdir(tmpDir, 0755); err != nil && !os.IsExist(err) {
    86  		return fmt.Errorf("failed to create directory: %v", err)
    87  	}
    88  	_, filename, _, _ := runtime.Caller(0)
    89  	baseDir := filepath.Dir(filename)
    90  	skeletonDir := filepath.Join(baseDir, "skeleton", "build")
    91  	mainTemplate, err := ioutil.ReadFile(filepath.Join(skeletonDir, "main.go"+util.TemplateSuffix))
    92  	if err != nil {
    93  		return err
    94  	}
    95  	mainFilePath := filepath.ToSlash(filepath.Join(tmpDir, "main.go"))
    96  	builderFilePath := filepath.ToSlash(filepath.Join(tmpDir, "builder.go"))
    97  	file, err := os.Create(builderFilePath)
    98  	if err != nil {
    99  		return fmt.Errorf("failed to create file: %v", err)
   100  	}
   101  	defer file.Close()
   102  	builderTemplatePath := filepath.ToSlash(filepath.Join(skeletonDir, "builder.go"+util.TemplateSuffix))
   103  	t := template.Must(template.ParseFiles(builderTemplatePath))
   104  	var resources map[string]string
   105  	if c.option.All {
   106  		resources = collectResourcePaths(filepath.Join(dir, kocha.StaticDir))
   107  	}
   108  	tag, err := c.detectVersionTag()
   109  	if err != nil {
   110  		return err
   111  	}
   112  	data := map[string]interface{}{
   113  		"configImportPath":    configPkg.ImportPath,
   114  		"dbImportPath":        dbImportPath,
   115  		"migrationImportPath": migrationImportPath,
   116  		"mainTemplate":        string(mainTemplate),
   117  		"mainFilePath":        mainFilePath,
   118  		"resources":           resources,
   119  		"version":             tag,
   120  	}
   121  	if err := t.Execute(file, data); err != nil {
   122  		return fmt.Errorf("failed to write file: %v", err)
   123  	}
   124  	file.Close()
   125  	execName := appName
   126  	if runtime.GOOS == "windows" {
   127  		execName += ".exe"
   128  	}
   129  	if err := execCmdWithHostEnv("go", "run", builderFilePath); err != nil {
   130  		return err
   131  	}
   132  	// To avoid to become a dynamic linked binary.
   133  	// See https://github.com/golang/go/issues/9344
   134  	execPath := filepath.Join(dir, execName)
   135  	execArgs := []string{"build", "-o", execPath, "-tags", "netgo", "-installsuffix", "netgo"}
   136  	// On Linux, works fine. On Windows, doesn't work.
   137  	// On other platforms, not tested.
   138  	if runtime.GOOS == "linux" {
   139  		execArgs = append(execArgs, "-ldflags", `-extldflags "-static"`)
   140  	}
   141  	execArgs = append(execArgs, mainFilePath)
   142  	if err := execCmd("go", execArgs...); err != nil {
   143  		return err
   144  	}
   145  	if err := os.RemoveAll(tmpDir); err != nil {
   146  		return err
   147  	}
   148  	if err := util.PrintEnv(dir); err != nil {
   149  		return err
   150  	}
   151  	fmt.Printf("build all-in-one binary to %v\n", execPath)
   152  	util.PrintGreen("Build successful!\n")
   153  	return nil
   154  }
   155  
   156  func getPackage(importPath string) (*build.Package, error) {
   157  	return build.Import(importPath, "", build.FindOnly)
   158  }
   159  
   160  func execCmd(cmd string, args ...string) error {
   161  	command := exec.Command(cmd, args...)
   162  	if msg, err := command.CombinedOutput(); err != nil {
   163  		return fmt.Errorf("build failed: %v\n%v", err, string(msg))
   164  	}
   165  	return nil
   166  }
   167  
   168  func execCmdWithHostEnv(cmd string, args ...string) (err error) {
   169  	targetGOOS, targetGOARCH := os.Getenv("GOOS"), os.Getenv("GOARCH")
   170  	for name, env := range map[string]string{
   171  		"GOOS":   runtime.GOOS,
   172  		"GOARCH": runtime.GOARCH,
   173  	} {
   174  		if err := os.Setenv(name, env); err != nil {
   175  			return err
   176  		}
   177  	}
   178  	defer func() {
   179  		for name, env := range map[string]string{
   180  			"GOOS":   targetGOOS,
   181  			"GOARCH": targetGOARCH,
   182  		} {
   183  			if e := os.Setenv(name, env); e != nil {
   184  				if err != nil {
   185  					err = e
   186  				}
   187  			}
   188  		}
   189  	}()
   190  	return execCmd(cmd, args...)
   191  }
   192  
   193  func collectResourcePaths(root string) map[string]string {
   194  	result := make(map[string]string)
   195  	filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
   196  		if err != nil {
   197  			return err
   198  		}
   199  		if info.Name()[0] == '.' {
   200  			if info.IsDir() {
   201  				return filepath.SkipDir
   202  			}
   203  			return nil
   204  		}
   205  		if info.IsDir() {
   206  			return nil
   207  		}
   208  		rel, err := filepath.Rel(root, path)
   209  		if err != nil {
   210  			return err
   211  		}
   212  		result[rel] = filepath.ToSlash(path)
   213  		return nil
   214  	})
   215  	return result
   216  }
   217  
   218  func (c *buildCommand) detectVersionTag() (string, error) {
   219  	if c.option.Tag != "" {
   220  		return c.option.Tag, nil
   221  	}
   222  	var repo string
   223  	for _, dir := range []string{".git", ".hg"} {
   224  		if info, err := os.Stat(dir); err == nil && info.IsDir() {
   225  			repo = dir
   226  			break
   227  		}
   228  	}
   229  	version := time.Now().Format(time.RFC1123Z)
   230  	switch repo {
   231  	case ".git":
   232  		bin, err := exec.LookPath("git")
   233  		if err != nil {
   234  			fmt.Fprintf(os.Stderr, "%s: WARNING: git repository found, but `git` command not found. use \"%s\" as version\n", c.Name(), version)
   235  			break
   236  		}
   237  		line, err := exec.Command(bin, "rev-parse", "HEAD").Output()
   238  		if err != nil {
   239  			return "", fmt.Errorf("unexpected error: %v\nplease specify the version using '--tag' option to avoid the this error", err)
   240  		}
   241  		version = strings.TrimSpace(string(line))
   242  	case ".hg":
   243  		bin, err := exec.LookPath("hg")
   244  		if err != nil {
   245  			fmt.Fprintf(os.Stderr, "%s: WARNING: hg repository found, but `hg` command not found. use \"%s\" as version\n", c.Name(), version)
   246  			break
   247  		}
   248  		line, err := exec.Command(bin, "identify").Output()
   249  		if err != nil {
   250  			return "", fmt.Errorf("unexpected error: %v\nplease specify version using '--tag' option to avoid the this error", err)
   251  		}
   252  		version = strings.TrimSpace(string(line))
   253  	}
   254  	if version == "" {
   255  		// Probably doesn't reach here.
   256  		version = time.Now().Format(time.RFC1123Z)
   257  		fmt.Fprintf(os.Stderr, `%s: WARNING: version is empty, use "%s" as version`, c.Name(), version)
   258  	}
   259  	return version, nil
   260  }
   261  
   262  func main() {
   263  	util.RunCommand(&buildCommand{})
   264  }