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 }