bee/pack.go

628 lines
13 KiB
Go

// Copyright 2013 bee authors
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package main
import (
"archive/tar"
"archive/zip"
"compress/gzip"
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
path "path/filepath"
"regexp"
"runtime"
"sort"
"strconv"
"strings"
"syscall"
"time"
)
var cmdPack = &Command{
CustomFlags: true,
UsageLine: "pack",
Short: "compress an beego project",
Long: `
compress an beego project
-p app path. default is current path
-b build specify platform app. default true
-ba additional args of go build
-be=[] additional ENV Variables of go build. eg: GOARCH=arm
-o compressed file output dir. default use current path
-f="" format. [ tar.gz / zip ]. default tar.gz
-exp="" relpath exclude prefix. default: .
-exs="" relpath exclude suffix. default: .go:.DS_Store:.tmp
all path use : as separator
-exr=[] file/directory name exclude by Regexp. default: ^.
-fs=false follow symlink. default false
-ss=false skip symlink. default false
default embed symlink into compressed file
-v=false verbose
`,
}
var (
appPath string
excludeP string
excludeS string
outputP string
excludeR ListOpts
fsym bool
ssym bool
build bool
buildArgs string
buildEnvs ListOpts
verbose bool
format string
)
type ListOpts []string
func (opts *ListOpts) String() string {
return fmt.Sprint(*opts)
}
func (opts *ListOpts) Set(value string) error {
*opts = append(*opts, value)
return nil
}
func init() {
fs := flag.NewFlagSet("pack", flag.ContinueOnError)
fs.StringVar(&appPath, "p", "", "app path. default is current path")
fs.BoolVar(&build, "b", true, "build specify platform app")
fs.StringVar(&buildArgs, "ba", "", "additional args of go build")
fs.Var(&buildEnvs, "be", "additional ENV Variables of go build. eg: GOARCH=arm")
fs.StringVar(&outputP, "o", "", "compressed file output dir. default use current path")
fs.StringVar(&format, "f", "tar.gz", "format. [ tar.gz / zip ]")
fs.StringVar(&excludeP, "exp", ".", "path exclude prefix. use : as separator")
fs.StringVar(&excludeS, "exs", ".go:.DS_Store:.tmp", "path exclude suffix. use : as separator")
fs.Var(&excludeR, "exr", "filename exclude by Regexp")
fs.BoolVar(&fsym, "fs", false, "follow symlink")
fs.BoolVar(&ssym, "ss", false, "skip symlink")
fs.BoolVar(&verbose, "v", false, "verbose")
cmdPack.Flag = *fs
cmdPack.Run = packApp
}
func exitPrint(con string) {
fmt.Fprintln(os.Stderr, con)
os.Exit(2)
}
type walker interface {
isExclude(string) bool
isEmpty(string) bool
relName(string) string
virPath(string) string
compress(string, string, os.FileInfo) (bool, error)
walkRoot(string) error
}
type byName []os.FileInfo
func (f byName) Len() int { return len(f) }
func (f byName) Less(i, j int) bool { return f[i].Name() < f[j].Name() }
func (f byName) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
type walkFileTree struct {
wak walker
prefix string
excludePrefix []string
excludeRegexp []*regexp.Regexp
excludeSuffix []string
allfiles map[string]bool
}
func (wft *walkFileTree) setPrefix(prefix string) {
wft.prefix = prefix
}
func (wft *walkFileTree) isExclude(fPath string) bool {
if fPath == "" {
return true
}
for _, prefix := range wft.excludePrefix {
if strings.HasPrefix(fPath, prefix) {
return true
}
}
for _, suffix := range wft.excludeSuffix {
if strings.HasSuffix(fPath, suffix) {
return true
}
}
return false
}
func (wft *walkFileTree) isExcludeName(name string) bool {
for _, r := range wft.excludeRegexp {
if r.MatchString(name) {
return true
}
}
return false
}
func (wft *walkFileTree) isEmpty(fpath string) bool {
fh, _ := os.Open(fpath)
defer fh.Close()
infos, _ := fh.Readdir(-1)
for _, fi := range infos {
fn := fi.Name()
fp := path.Join(fpath, fn)
if wft.isExclude(wft.virPath(fp)) {
continue
}
if wft.isExcludeName(fn) {
continue
}
if fi.Mode()&os.ModeSymlink > 0 {
continue
}
if fi.IsDir() && wft.isEmpty(fp) {
continue
}
return false
}
return true
}
func (wft *walkFileTree) relName(fpath string) string {
name, _ := path.Rel(wft.prefix, fpath)
return name
}
func (wft *walkFileTree) virPath(fpath string) string {
name := fpath[len(wft.prefix):]
if name == "" {
return ""
}
name = name[1:]
return name
}
func (wft *walkFileTree) readDir(dirname string) ([]os.FileInfo, error) {
f, err := os.Open(dirname)
if err != nil {
return nil, err
}
list, err := f.Readdir(-1)
f.Close()
if err != nil {
return nil, err
}
sort.Sort(byName(list))
return list, nil
}
func (wft *walkFileTree) walkLeaf(fpath string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
if fpath == outputP {
return nil
}
if fi.IsDir() {
return nil
}
if ssym && fi.Mode()&os.ModeSymlink > 0 {
return nil
}
name := wft.virPath(fpath)
if wft.allfiles[name] {
return nil
}
if added, err := wft.wak.compress(name, fpath, fi); added {
if verbose {
fmt.Printf("Compressed: %s\n", name)
}
wft.allfiles[name] = true
return err
} else {
return err
}
}
func (wft *walkFileTree) iterDirectory(fpath string, fi os.FileInfo) error {
doFSym := fsym && fi.Mode()&os.ModeSymlink > 0
if doFSym {
nfi, err := os.Stat(fpath)
if os.IsNotExist(err) {
return nil
}
fi = nfi
}
relPath := wft.virPath(fpath)
if len(relPath) > 0 {
if wft.isExcludeName(fi.Name()) {
return nil
}
if wft.isExclude(relPath) {
return nil
}
}
err := wft.walkLeaf(fpath, fi, nil)
if err != nil {
if fi.IsDir() && err == path.SkipDir {
return nil
}
return err
}
if !fi.IsDir() {
return nil
}
list, err := wft.readDir(fpath)
if err != nil {
return wft.walkLeaf(fpath, fi, err)
}
for _, fileInfo := range list {
err = wft.iterDirectory(path.Join(fpath, fileInfo.Name()), fileInfo)
if err != nil {
if !fileInfo.IsDir() || err != path.SkipDir {
return err
}
}
}
return nil
}
func (wft *walkFileTree) walkRoot(root string) error {
wft.prefix = root
fi, err := os.Stat(root)
if err != nil {
return err
}
return wft.iterDirectory(root, fi)
}
type tarWalk struct {
walkFileTree
tw *tar.Writer
}
func (wft *tarWalk) compress(name, fpath string, fi os.FileInfo) (bool, error) {
isSym := fi.Mode()&os.ModeSymlink > 0
link := ""
if isSym {
link, _ = os.Readlink(fpath)
}
hdr, err := tar.FileInfoHeader(fi, link)
if err != nil {
return false, err
}
hdr.Name = name
tw := wft.tw
err = tw.WriteHeader(hdr)
if err != nil {
return false, err
}
if isSym == false {
fr, err := os.Open(fpath)
if err != nil {
return false, err
}
defer fr.Close()
_, err = io.Copy(tw, fr)
if err != nil {
return false, err
}
tw.Flush()
}
return true, nil
}
type zipWalk struct {
walkFileTree
zw *zip.Writer
}
func (wft *zipWalk) compress(name, fpath string, fi os.FileInfo) (bool, error) {
isSym := fi.Mode()&os.ModeSymlink > 0
hdr, err := zip.FileInfoHeader(fi)
if err != nil {
return false, err
}
hdr.Name = name
zw := wft.zw
w, err := zw.CreateHeader(hdr)
if err != nil {
return false, err
}
if isSym == false {
fr, err := os.Open(fpath)
if err != nil {
return false, err
}
defer fr.Close()
_, err = io.Copy(w, fr)
if err != nil {
return false, err
}
} else {
var link string
if link, err = os.Readlink(fpath); err != nil {
return false, err
}
_, err = w.Write([]byte(link))
if err != nil {
return false, err
}
}
return true, nil
}
func packDirectory(excludePrefix []string, excludeSuffix []string,
excludeRegexp []*regexp.Regexp, includePath ...string) (err error) {
fmt.Printf("exclude relpath prefix: %s\n", strings.Join(excludePrefix, ":"))
fmt.Printf("exclude relpath suffix: %s\n", strings.Join(excludeSuffix, ":"))
if len(excludeRegexp) > 0 {
fmt.Printf("exclude filename regex: `%s`\n", strings.Join(excludeR, "`, `"))
}
w, err := os.OpenFile(outputP, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return err
}
var wft walker
if format == "zip" {
walk := new(zipWalk)
zw := zip.NewWriter(w)
defer func() {
zw.Close()
}()
walk.allfiles = make(map[string]bool)
walk.zw = zw
walk.wak = walk
walk.excludePrefix = excludePrefix
walk.excludeSuffix = excludeSuffix
walk.excludeRegexp = excludeRegexp
wft = walk
} else {
walk := new(tarWalk)
cw := gzip.NewWriter(w)
tw := tar.NewWriter(cw)
defer func() {
tw.Flush()
cw.Flush()
tw.Close()
cw.Close()
}()
walk.allfiles = make(map[string]bool)
walk.tw = tw
walk.wak = walk
walk.excludePrefix = excludePrefix
walk.excludeSuffix = excludeSuffix
walk.excludeRegexp = excludeRegexp
wft = walk
}
for _, p := range includePath {
err = wft.walkRoot(p)
if err != nil {
return
}
}
return
}
func isBeegoProject(thePath string) bool {
fh, _ := os.Open(thePath)
fis, _ := fh.Readdir(-1)
regex := regexp.MustCompile(`(?s)package main.*?import.*?\(.*?"github.com/astaxie/beego".*?\).*func main()`)
for _, fi := range fis {
if fi.IsDir() == false && strings.HasSuffix(fi.Name(), ".go") {
data, err := ioutil.ReadFile(path.Join(thePath, fi.Name()))
if err != nil {
continue
}
if len(regex.Find(data)) > 0 {
return true
}
}
}
return false
}
func packApp(cmd *Command, args []string) int {
curPath, _ := os.Getwd()
thePath := ""
nArgs := []string{}
has := false
for _, a := range args {
if a != "" && a[0] == '-' {
has = true
}
if has {
nArgs = append(nArgs, a)
}
}
cmdPack.Flag.Parse(nArgs)
if path.IsAbs(appPath) == false {
appPath = path.Join(curPath, appPath)
}
thePath, err := path.Abs(appPath)
if err != nil {
exitPrint(fmt.Sprintf("wrong app path: %s", thePath))
}
if stat, err := os.Stat(thePath); os.IsNotExist(err) || stat.IsDir() == false {
exitPrint(fmt.Sprintf("not exist app path: %s", thePath))
}
if isBeegoProject(thePath) == false {
exitPrint(fmt.Sprintf("not support non beego project"))
}
fmt.Printf("app path: %s\n", thePath)
appName := path.Base(thePath)
goos := runtime.GOOS
if v, found := syscall.Getenv("GOOS"); found {
goos = v
}
goarch := runtime.GOARCH
if v, found := syscall.Getenv("GOARCH"); found {
goarch = v
}
str := strconv.FormatInt(time.Now().UnixNano(), 10)[9:]
tmpdir := path.Join(os.TempDir(), "beePack-"+str)
os.Mkdir(tmpdir, 0700)
if build {
fmt.Println("build", appName)
var envs []string
for _, env := range buildEnvs {
parts := strings.SplitN(env, "=", 2)
if len(parts) == 2 {
k, v := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
if len(k) > 0 && len(v) > 0 {
switch k {
case "GOOS":
goos = v
case "GOARCH":
goarch = v
default:
envs = append(envs, fmt.Sprintf("%s=%s", k, v))
}
}
}
}
os.Setenv("GOOS", goos)
os.Setenv("GOARCH", goarch)
fmt.Println("GOOS", goos, "GOARCH", goarch)
binPath := path.Join(tmpdir, appName)
if goos == "windows" {
binPath += ".exe"
}
args := []string{"build", "-o", binPath}
if len(buildArgs) > 0 {
args = append(args, strings.Fields(buildArgs)...)
}
if verbose {
fmt.Println("go ", strings.Join(args, " "))
}
execmd := exec.Command("go", args...)
execmd.Env = append(os.Environ(), envs...)
execmd.Stdout = os.Stdout
execmd.Stderr = os.Stderr
execmd.Dir = thePath
err = execmd.Run()
if err != nil {
exitPrint(err.Error())
}
fmt.Println("build success")
}
switch format {
case "zip":
default:
format = "tar.gz"
}
outputN := appName + "." + format
if outputP == "" || path.IsAbs(outputP) == false {
outputP = path.Join(curPath, outputP)
}
if _, err := os.Stat(outputP); err != nil {
err = os.MkdirAll(outputP, 0755)
if err != nil {
exitPrint(err.Error())
}
}
outputP = path.Join(outputP, outputN)
var exp, exs []string
for _, p := range strings.Split(excludeP, ":") {
if len(p) > 0 {
exp = append(exp, p)
}
}
for _, p := range strings.Split(excludeS, ":") {
if len(p) > 0 {
exs = append(exs, p)
}
}
var exr []*regexp.Regexp
for _, r := range excludeR {
if len(r) > 0 {
if re, err := regexp.Compile(r); err != nil {
exitPrint(err.Error())
} else {
exr = append(exr, re)
}
}
}
err = packDirectory(exp, exs, exr, tmpdir, thePath)
if err != nil {
exitPrint(err.Error())
}
fmt.Printf("file write to `%s`\n", outputP)
return 0
}