mirror of
https://github.com/kevin-DL/revel-cmd.git
synced 2026-01-11 18:54:31 +00:00
Reformat of code Allow user to use a mix of command line arguments and flags Enhance the import tool to detect missing packages in the modules side Added test cases for all commands
553 lines
16 KiB
Go
553 lines
16 KiB
Go
// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
|
|
// Revel Framework source code and usage is governed by a MIT style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package harness
|
|
|
|
import (
|
|
"fmt"
|
|
"go/build"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/revel/cmd/model"
|
|
"github.com/revel/cmd/parser"
|
|
"github.com/revel/cmd/utils"
|
|
)
|
|
|
|
var importErrorPattern = regexp.MustCompile("cannot find package \"([^\"]+)\"")
|
|
|
|
type ByString []*model.TypeInfo
|
|
|
|
func (c ByString) Len() int { return len(c) }
|
|
func (c ByString) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
|
|
func (c ByString) Less(i, j int) bool { return c[i].String() < c[j].String() }
|
|
|
|
// Build the app:
|
|
// 1. Generate the the main.go file.
|
|
// 2. Run the appropriate "go build" command.
|
|
// Requires that revel.Init has been called previously.
|
|
// Returns the path to the built binary, and an error if there was a problem building it.
|
|
func Build(c *model.CommandConfig, paths *model.RevelContainer) (_ *App, err error) {
|
|
// First, clear the generated files (to avoid them messing with ProcessSource).
|
|
cleanSource(paths, "tmp", "routes")
|
|
|
|
sourceInfo, err := parser.ProcessSource(paths)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Add the db.import to the import paths.
|
|
if dbImportPath, found := paths.Config.String("db.import"); found {
|
|
sourceInfo.InitImportPaths = append(sourceInfo.InitImportPaths, strings.Split(dbImportPath, ",")...)
|
|
}
|
|
|
|
// Sort controllers so that file generation is reproducible
|
|
controllers := sourceInfo.ControllerSpecs()
|
|
sort.Stable(ByString(controllers))
|
|
|
|
// Generate two source files.
|
|
templateArgs := map[string]interface{}{
|
|
"ImportPath": paths.ImportPath,
|
|
"Controllers": controllers,
|
|
"ValidationKeys": sourceInfo.ValidationKeys,
|
|
"ImportPaths": calcImportAliases(sourceInfo),
|
|
"TestSuites": sourceInfo.TestSuites(),
|
|
}
|
|
|
|
// Generate code for the main, run and routes file.
|
|
// The run file allows external programs to launch and run the application
|
|
// without being the main thread
|
|
cleanSource(paths, "tmp", "routes")
|
|
|
|
if err = genSource(paths, "tmp", "main.go", RevelMainTemplate, templateArgs); err != nil {
|
|
return
|
|
}
|
|
if err = genSource(paths, filepath.Join("tmp", "run"), "run.go", RevelRunTemplate, templateArgs); err != nil {
|
|
return
|
|
}
|
|
if err = genSource(paths, "routes", "routes.go", RevelRoutesTemplate, templateArgs); err != nil {
|
|
return
|
|
}
|
|
|
|
// Read build config.
|
|
buildTags := paths.Config.StringDefault("build.tags", "")
|
|
|
|
// Build the user program (all code under app).
|
|
// It relies on the user having "go" installed.
|
|
goPath, err := exec.LookPath("go")
|
|
if err != nil {
|
|
utils.Logger.Fatal("Go executable not found in PATH.")
|
|
}
|
|
|
|
// Detect if deps tool should be used (is there a vendor folder ?)
|
|
useVendor := utils.DirExists(filepath.Join(paths.BasePath, "vendor"))
|
|
basePath := paths.BasePath
|
|
for !useVendor {
|
|
basePath = filepath.Dir(basePath)
|
|
found := false
|
|
// Check to see if we are still in the GOPATH
|
|
for _, gopath := range filepath.SplitList(build.Default.GOPATH) {
|
|
if strings.HasPrefix(basePath, gopath) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
break
|
|
} else {
|
|
useVendor = utils.DirExists(filepath.Join(basePath, "vendor"))
|
|
}
|
|
}
|
|
|
|
pkg, err := build.Default.Import(paths.ImportPath, "", build.FindOnly)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Binary path is a combination of $GOBIN/revel.d directory, app's import path and its name.
|
|
binName := filepath.Join(pkg.BinDir, "revel.d", paths.ImportPath, filepath.Base(paths.BasePath))
|
|
|
|
// Change binary path for Windows build
|
|
goos := runtime.GOOS
|
|
if goosEnv := os.Getenv("GOOS"); goosEnv != "" {
|
|
goos = goosEnv
|
|
}
|
|
if goos == "windows" {
|
|
binName += ".exe"
|
|
}
|
|
|
|
gotten := make(map[string]struct{})
|
|
contains := func(s []string, e string) bool {
|
|
for _, a := range s {
|
|
if a == e {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
for {
|
|
appVersion := getAppVersion(paths)
|
|
|
|
buildTime := time.Now().UTC().Format(time.RFC3339)
|
|
versionLinkerFlags := fmt.Sprintf("-X %s/app.AppVersion=%s -X %s/app.BuildTime=%s",
|
|
paths.ImportPath, appVersion, paths.ImportPath, buildTime)
|
|
|
|
// Append any build flags specified, they will override existing flags
|
|
flags := []string{}
|
|
if len(c.BuildFlags) == 0 {
|
|
flags = []string{
|
|
"build",
|
|
"-i",
|
|
"-ldflags", versionLinkerFlags,
|
|
"-tags", buildTags,
|
|
"-o", binName}
|
|
} else {
|
|
if !contains(c.BuildFlags, "build") {
|
|
flags = []string{"build"}
|
|
}
|
|
flags = append(flags, c.BuildFlags...)
|
|
if !contains(flags, "-ldflags") {
|
|
flags = append(flags, "-ldflags", versionLinkerFlags)
|
|
}
|
|
if !contains(flags, "-tags") {
|
|
flags = append(flags, "-tags", buildTags)
|
|
}
|
|
if !contains(flags, "-o") {
|
|
flags = append(flags, "-o", binName)
|
|
}
|
|
}
|
|
|
|
// Add in build flags
|
|
flags = append(flags, c.BuildFlags...)
|
|
|
|
// This is Go main path
|
|
gopath := c.GoPath
|
|
for _, o := range paths.ModulePathMap {
|
|
gopath += string(filepath.ListSeparator) + o
|
|
}
|
|
|
|
// Note: It's not applicable for filepath.* usage
|
|
flags = append(flags, path.Join(paths.ImportPath, "app", "tmp"))
|
|
|
|
buildCmd := exec.Command(goPath, flags...)
|
|
buildCmd.Env = append(os.Environ(),
|
|
"GOPATH="+gopath,
|
|
)
|
|
utils.CmdInit(buildCmd, c.AppPath)
|
|
utils.Logger.Info("Exec:", "args", buildCmd.Args)
|
|
output, err := buildCmd.CombinedOutput()
|
|
|
|
// If the build succeeded, we're done.
|
|
if err == nil {
|
|
utils.Logger.Info("Build successful continuing")
|
|
return NewApp(binName, paths), nil
|
|
}
|
|
|
|
// Since there was an error, capture the output in case we need to report it
|
|
stOutput := string(output)
|
|
|
|
// See if it was an import error that we can go get.
|
|
matches := importErrorPattern.FindAllStringSubmatch(stOutput, -1)
|
|
utils.Logger.Info("Build failed checking for missing imports", "message", stOutput, "missing_imports", len(matches))
|
|
if matches == nil {
|
|
utils.Logger.Info("Build failed no missing imports", "message", stOutput)
|
|
return nil, newCompileError(paths, output)
|
|
}
|
|
utils.Logger.Warn("Detected missing packages, importing them", "packages", len(matches))
|
|
for _, match := range matches {
|
|
// Ensure we haven't already tried to go get it.
|
|
pkgName := match[1]
|
|
utils.Logger.Info("Trying to import ", "package", pkgName)
|
|
if _, alreadyTried := gotten[pkgName]; alreadyTried {
|
|
utils.Logger.Error("Failed to import ", "package", pkgName)
|
|
return nil, newCompileError(paths, output)
|
|
}
|
|
gotten[pkgName] = struct{}{}
|
|
if err := c.PackageResolver(pkgName); err != nil {
|
|
utils.Logger.Error("Unable to resolve package", "package", pkgName, "error", err)
|
|
return nil, newCompileError(paths, []byte(err.Error()))
|
|
}
|
|
}
|
|
|
|
// Success getting the import, attempt to build again.
|
|
}
|
|
|
|
// TODO remove this unreachable code and document it
|
|
utils.Logger.Fatal("Not reachable")
|
|
return nil, nil
|
|
}
|
|
|
|
// Try to define a version string for the compiled app
|
|
// The following is tried (first match returns):
|
|
// - Read a version explicitly specified in the APP_VERSION environment
|
|
// variable
|
|
// - Read the output of "git describe" if the source is in a git repository
|
|
// If no version can be determined, an empty string is returned.
|
|
func getAppVersion(paths *model.RevelContainer) string {
|
|
if version := os.Getenv("APP_VERSION"); version != "" {
|
|
return version
|
|
}
|
|
|
|
// Check for the git binary
|
|
if gitPath, err := exec.LookPath("git"); err == nil {
|
|
// Check for the .git directory
|
|
gitDir := filepath.Join(paths.BasePath, ".git")
|
|
info, err := os.Stat(gitDir)
|
|
if (err != nil && os.IsNotExist(err)) || !info.IsDir() {
|
|
return ""
|
|
}
|
|
gitCmd := exec.Command(gitPath, "--git-dir="+gitDir, "--work-tree="+paths.BasePath, "describe", "--always", "--dirty")
|
|
utils.Logger.Info("Exec:", "args", gitCmd.Args)
|
|
output, err := gitCmd.Output()
|
|
|
|
if err != nil {
|
|
utils.Logger.Error("Cannot determine git repository version:", "error", err)
|
|
return ""
|
|
}
|
|
|
|
return "git-" + strings.TrimSpace(string(output))
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func cleanSource(paths *model.RevelContainer, dirs ...string) {
|
|
for _, dir := range dirs {
|
|
cleanDir(paths, dir)
|
|
}
|
|
}
|
|
|
|
func cleanDir(paths *model.RevelContainer, dir string) {
|
|
utils.Logger.Info("Cleaning dir ", "dir", dir)
|
|
tmpPath := filepath.Join(paths.AppPath, dir)
|
|
f, err := os.Open(tmpPath)
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
utils.Logger.Error("Failed to clean dir:", "error", err)
|
|
}
|
|
} else {
|
|
defer func() {
|
|
_ = f.Close()
|
|
}()
|
|
|
|
infos, err := f.Readdir(0)
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
utils.Logger.Fatal("Failed to clean dir:", "error", err)
|
|
}
|
|
} else {
|
|
for _, info := range infos {
|
|
pathName := filepath.Join(tmpPath, info.Name())
|
|
if info.IsDir() {
|
|
err := os.RemoveAll(pathName)
|
|
if err != nil {
|
|
utils.Logger.Fatal("Failed to remove dir:", "error", err)
|
|
}
|
|
} else {
|
|
err := os.Remove(pathName)
|
|
if err != nil {
|
|
utils.Logger.Fatal("Failed to remove file:", "error", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// genSource renders the given template to produce source code, which it writes
|
|
// to the given directory and file.
|
|
func genSource(paths *model.RevelContainer, dir, filename, templateSource string, args map[string]interface{}) error {
|
|
|
|
return utils.GenerateTemplate(filepath.Join(paths.AppPath, dir, filename), templateSource, args)
|
|
}
|
|
|
|
// Looks through all the method args and returns a set of unique import paths
|
|
// that cover all the method arg types.
|
|
// Additionally, assign package aliases when necessary to resolve ambiguity.
|
|
func calcImportAliases(src *model.SourceInfo) map[string]string {
|
|
aliases := make(map[string]string)
|
|
typeArrays := [][]*model.TypeInfo{src.ControllerSpecs(), src.TestSuites()}
|
|
for _, specs := range typeArrays {
|
|
for _, spec := range specs {
|
|
addAlias(aliases, spec.ImportPath, spec.PackageName)
|
|
|
|
for _, methSpec := range spec.MethodSpecs {
|
|
for _, methArg := range methSpec.Args {
|
|
if methArg.ImportPath == "" {
|
|
continue
|
|
}
|
|
|
|
addAlias(aliases, methArg.ImportPath, methArg.TypeExpr.PkgName)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add the "InitImportPaths", with alias "_"
|
|
for _, importPath := range src.InitImportPaths {
|
|
if _, ok := aliases[importPath]; !ok {
|
|
aliases[importPath] = "_"
|
|
}
|
|
}
|
|
|
|
return aliases
|
|
}
|
|
|
|
// Adds an alias to the map of alias names
|
|
func addAlias(aliases map[string]string, importPath, pkgName string) {
|
|
alias, ok := aliases[importPath]
|
|
if ok {
|
|
return
|
|
}
|
|
alias = makePackageAlias(aliases, pkgName)
|
|
aliases[importPath] = alias
|
|
}
|
|
|
|
// Generates a package alias
|
|
func makePackageAlias(aliases map[string]string, pkgName string) string {
|
|
i := 0
|
|
alias := pkgName
|
|
for containsValue(aliases, alias) || alias == "revel" {
|
|
alias = fmt.Sprintf("%s%d", pkgName, i)
|
|
i++
|
|
}
|
|
return alias
|
|
}
|
|
|
|
// Returns true if this value is in the map
|
|
func containsValue(m map[string]string, val string) bool {
|
|
for _, v := range m {
|
|
if v == val {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Parse the output of the "go build" command.
|
|
// Return a detailed Error.
|
|
func newCompileError(paths *model.RevelContainer, output []byte) *utils.Error {
|
|
errorMatch := regexp.MustCompile(`(?m)^([^:#]+):(\d+):(\d+:)? (.*)$`).
|
|
FindSubmatch(output)
|
|
if errorMatch == nil {
|
|
errorMatch = regexp.MustCompile(`(?m)^(.*?):(\d+):\s(.*?)$`).FindSubmatch(output)
|
|
|
|
if errorMatch == nil {
|
|
utils.Logger.Error("Failed to parse build errors", "error", string(output))
|
|
return &utils.Error{
|
|
SourceType: "Go code",
|
|
Title: "Go Compilation Error",
|
|
Description: "See console for build error.",
|
|
}
|
|
}
|
|
|
|
errorMatch = append(errorMatch, errorMatch[3])
|
|
|
|
utils.Logger.Error("Build errors", "errors", string(output))
|
|
}
|
|
|
|
findInPaths := func(relFilename string) string {
|
|
// Extract the paths from the gopaths, and search for file there first
|
|
gopaths := filepath.SplitList(build.Default.GOPATH)
|
|
for _, gp := range gopaths {
|
|
newPath := filepath.Join(gp,"src", paths.ImportPath, relFilename)
|
|
println(newPath)
|
|
if utils.Exists(newPath) {
|
|
return newPath
|
|
}
|
|
}
|
|
newPath, _ := filepath.Abs(relFilename)
|
|
utils.Logger.Warn("Could not find in GO path", "file", relFilename)
|
|
return newPath
|
|
}
|
|
|
|
|
|
// Read the source for the offending file.
|
|
var (
|
|
relFilename = string(errorMatch[1]) // e.g. "src/revel/sample/app/controllers/app.go"
|
|
absFilename = findInPaths(relFilename)
|
|
line, _ = strconv.Atoi(string(errorMatch[2]))
|
|
description = string(errorMatch[4])
|
|
compileError = &utils.Error{
|
|
SourceType: "Go code",
|
|
Title: "Go Compilation Error",
|
|
Path: relFilename,
|
|
Description: description,
|
|
Line: line,
|
|
}
|
|
)
|
|
|
|
errorLink := paths.Config.StringDefault("error.link", "")
|
|
|
|
if errorLink != "" {
|
|
compileError.SetLink(errorLink)
|
|
}
|
|
|
|
fileStr, err := utils.ReadLines(absFilename)
|
|
if err != nil {
|
|
compileError.MetaError = absFilename + ": " + err.Error()
|
|
utils.Logger.Info("Unable to readlines "+compileError.MetaError, "error", err)
|
|
return compileError
|
|
}
|
|
|
|
compileError.SourceLines = fileStr
|
|
return compileError
|
|
}
|
|
|
|
// RevelMainTemplate template for app/tmp/main.go
|
|
const RevelRunTemplate = `// GENERATED CODE - DO NOT EDIT
|
|
// This file is the run file for Revel.
|
|
// It registers all the controllers and provides details for the Revel server engine to
|
|
// properly inject parameters directly into the action endpoints.
|
|
package run
|
|
|
|
import (
|
|
"reflect"
|
|
"github.com/revel/revel"{{range $k, $v := $.ImportPaths}}
|
|
{{$v}} "{{$k}}"{{end}}
|
|
"github.com/revel/revel/testing"
|
|
)
|
|
|
|
var (
|
|
// So compiler won't complain if the generated code doesn't reference reflect package...
|
|
_ = reflect.Invalid
|
|
)
|
|
|
|
// Register and run the application
|
|
func Run(port int) {
|
|
Register()
|
|
revel.Run(port)
|
|
}
|
|
|
|
// Register all the controllers
|
|
func Register() {
|
|
revel.AppLog.Info("Running revel server")
|
|
{{range $i, $c := .Controllers}}
|
|
revel.RegisterController((*{{index $.ImportPaths .ImportPath}}.{{.StructName}})(nil),
|
|
[]*revel.MethodType{
|
|
{{range .MethodSpecs}}&revel.MethodType{
|
|
Name: "{{.Name}}",
|
|
Args: []*revel.MethodArg{ {{range .Args}}
|
|
&revel.MethodArg{Name: "{{.Name}}", Type: reflect.TypeOf((*{{index $.ImportPaths .ImportPath | .TypeExpr.TypeName}})(nil)) },{{end}}
|
|
},
|
|
RenderArgNames: map[int][]string{ {{range .RenderCalls}}
|
|
{{.Line}}: []string{ {{range .Names}}
|
|
"{{.}}",{{end}}
|
|
},{{end}}
|
|
},
|
|
},
|
|
{{end}}
|
|
})
|
|
{{end}}
|
|
revel.DefaultValidationKeys = map[string]map[int]string{ {{range $path, $lines := .ValidationKeys}}
|
|
"{{$path}}": { {{range $line, $key := $lines}}
|
|
{{$line}}: "{{$key}}",{{end}}
|
|
},{{end}}
|
|
}
|
|
testing.TestSuites = []interface{}{ {{range .TestSuites}}
|
|
(*{{index $.ImportPaths .ImportPath}}.{{.StructName}})(nil),{{end}}
|
|
}
|
|
}
|
|
`
|
|
const RevelMainTemplate = `// GENERATED CODE - DO NOT EDIT
|
|
// This file is the main file for Revel.
|
|
// It registers all the controllers and provides details for the Revel server engine to
|
|
// properly inject parameters directly into the action endpoints.
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"{{.ImportPath}}/app/tmp/run"
|
|
"github.com/revel/revel"
|
|
)
|
|
|
|
var (
|
|
runMode *string = flag.String("runMode", "", "Run mode.")
|
|
port *int = flag.Int("port", 0, "By default, read from app.conf")
|
|
importPath *string = flag.String("importPath", "", "Go Import Path for the app.")
|
|
srcPath *string = flag.String("srcPath", "", "Path to the source root.")
|
|
|
|
)
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
revel.Init(*runMode, *importPath, *srcPath)
|
|
run.Run(*port)
|
|
}
|
|
`
|
|
|
|
// RevelRoutesTemplate template for app/conf/routes
|
|
const RevelRoutesTemplate = `// GENERATED CODE - DO NOT EDIT
|
|
// This file provides a way of creating URL's based on all the actions
|
|
// found in all the controllers.
|
|
package routes
|
|
|
|
import "github.com/revel/revel"
|
|
|
|
{{range $i, $c := .Controllers}}
|
|
type t{{.StructName}} struct {}
|
|
var {{.StructName}} t{{.StructName}}
|
|
|
|
{{range .MethodSpecs}}
|
|
func (_ t{{$c.StructName}}) {{.Name}}({{range .Args}}
|
|
{{.Name}} {{if .ImportPath}}interface{}{{else}}{{.TypeExpr.TypeName ""}}{{end}},{{end}}
|
|
) string {
|
|
args := make(map[string]string)
|
|
{{range .Args}}
|
|
revel.Unbind(args, "{{.Name}}", {{.Name}}){{end}}
|
|
return revel.MainRouter.Reverse("{{$c.StructName}}.{{.Name}}", args).URL
|
|
}
|
|
{{end}}
|
|
{{end}}
|
|
`
|