diff --git a/.codebeatsettings b/.codebeatsettings index f921ed8..9fe4c72 100644 --- a/.codebeatsettings +++ b/.codebeatsettings @@ -1,6 +1,6 @@ { "GOLANG": { - "ABC":[15, 25, 50, 70], + "ABC":[25, 35, 50, 70], "BLOCK_NESTING":[5, 6, 7, 8], "CYCLO":[20, 30, 45, 60], "TOO_MANY_IVARS": [15, 18, 20, 25], diff --git a/.travis.yml b/.travis.yml index 87fcf17..057cc9e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,15 @@ language: go go: - - "1.8" + - "1.8.7" - "1.9" - "1.10" - "1.11" - "tip" os: - - linux - osx + - linux - windows sudo: false @@ -19,19 +19,6 @@ branches: - master - develop -services: - - memcache - - redis-server - -before_install: - # TRAVIS_OS_NAME - linux and osx - - echo $TRAVIS_OS_NAME - - | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then - brew update && brew install memcached redis && brew services start redis && brew services start memcached - fi - - redis-server --daemonize yes - - redis-cli info install: # Setting environments variables @@ -39,17 +26,13 @@ install: - export REVEL_BRANCH="develop" - 'if [[ "$TRAVIS_BRANCH" == "master" ]]; then export REVEL_BRANCH="master"; fi' - 'echo "Travis branch: $TRAVIS_BRANCH, Revel dependency branch: $REVEL_BRANCH"' - - git clone -b $REVEL_BRANCH git://github.com/revel/modules ../modules/ - - git clone -b $REVEL_BRANCH git://github.com/revel/revel ../revel/ - - git clone -b $REVEL_BRANCH git://github.com/revel/config ../config/ - - git clone -b $REVEL_BRANCH git://github.com/revel/cron ../cron/ - - git clone -b $REVEL_BRANCH git://github.com/revel/examples ../examples/ - - go get -v github.com/revel/revel/... - - go get -v github.com/revel/cmd/revel + - go get -t -v github.com/revel/cmd/revel - go get -u github.com/golang/dep/cmd/dep - + - echo $GOPATH + - echo $PATH + - pwd script: - - go test -v github.com/revel/cmd/... + - go test -v github.com/revel/cmd/revel/... # Ensure the new-app flow works (plus the other commands). - revel version @@ -65,21 +48,19 @@ script: - revel new -a my/testapp2 - revel test -a my/testapp2 - revel clean -a my/testapp2 - - revel build -a my/testapp2 -t build/testapp - - revel build -a my/testapp2 -t build/testapp -m prod + - revel build -a my/testapp2 -t build/testapp2 + - revel build -a my/testapp2 -t build/testapp2 -m prod - revel package -a my/testapp2 - revel package -a my/testapp2 -m prod - - revel new -a my/testapp3 -V - - revel test -a my/testapp3 - - revel clean -a my/testapp3 - - revel build -a my/testapp3 -t build/testapp - - revel build -a my/testapp3 -t build/testapp -m prod + - revel new -v -a my/testapp3 -V + - revel test -v -a my/testapp3 + - revel clean -v -a my/testapp3 + - revel build -a my/testapp3 -t build/testapp3 + - revel build -a my/testapp3 -t build/testapp3 -m prod - revel package -a my/testapp3 - revel package -a my/testapp3 -m prod matrix: allow_failures: - go: tip - - go: 1.6 - os: osx diff --git a/harness/app.go b/harness/app.go index 2765918..05cc315 100644 --- a/harness/app.go +++ b/harness/app.go @@ -61,28 +61,30 @@ func NewAppCmd(binPath string, port int, runMode string, paths *model.RevelConta // Start the app server, and wait until it is ready to serve requests. func (cmd AppCmd) Start(c *model.CommandConfig) error { - listeningWriter := &startupListeningWriter{os.Stdout, make(chan bool),c} + listeningWriter := &startupListeningWriter{os.Stdout, make(chan bool), c} cmd.Stdout = listeningWriter - utils.Logger.Info("Exec app:", "path", cmd.Path, "args", cmd.Args) + utils.Logger.Info("Exec app:", "path", cmd.Path, "args", cmd.Args, "dir", cmd.Dir, "env", cmd.Env) + utils.CmdInit(cmd.Cmd, c.AppPath) if err := cmd.Cmd.Start(); err != nil { utils.Logger.Fatal("Error running:", "error", err) } select { case exitState := <-cmd.waitChan(): + println("Revel proxy is listening, point your browser to :", c.Run.Port) return errors.New("revel/harness: app died reason: " + exitState) case <-time.After(60 * time.Second): + println("Revel proxy is listening, point your browser to :", c.Run.Port) utils.Logger.Error("Killing revel server process did not respond after wait timeout.", "processid", cmd.Process.Pid) cmd.Kill() return errors.New("revel/harness: app timed out") case <-listeningWriter.notifyReady: + println("Revel proxy is listening, point your browser to :", c.Run.Port) return nil } - // TODO remove this unreachable code and document it - panic("Impossible") } // Run the app server inline. Never returns. @@ -111,7 +113,7 @@ func (cmd AppCmd) waitChan() <-chan string { _ = cmd.Wait() state := cmd.ProcessState exitStatus := " unknown " - if state!=nil { + if state != nil { exitStatus = state.String() } @@ -126,7 +128,7 @@ func (cmd AppCmd) waitChan() <-chan string { type startupListeningWriter struct { dest io.Writer notifyReady chan bool - c *model.CommandConfig + c *model.CommandConfig } func (w *startupListeningWriter) Write(p []byte) (int, error) { diff --git a/harness/build.go b/harness/build.go index 65299ed..c2c4228 100644 --- a/harness/build.go +++ b/harness/build.go @@ -36,12 +36,12 @@ func (c ByString) Less(i, j int) bool { return c[i].String() < c[j].String() } // 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 *App, compileError *utils.Error) { +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, compileError := parser.ProcessSource(paths) - if compileError != nil { + sourceInfo, err := parser.ProcessSource(paths) + if err != nil { return } @@ -68,9 +68,15 @@ func Build(c *model.CommandConfig, paths *model.RevelContainer) (app *App, compi // without being the main thread cleanSource(paths, "tmp", "routes") - genSource(paths, "tmp", "main.go", RevelMainTemplate, templateArgs) - genSource(paths, filepath.Join("tmp", "run"), "run.go", RevelRunTemplate, templateArgs) - genSource(paths, "routes", "routes.go", RevelRoutesTemplate, templateArgs) + 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", "") @@ -102,23 +108,9 @@ func Build(c *model.CommandConfig, paths *model.RevelContainer) (app *App, compi } } - var depPath string - if useVendor { - utils.Logger.Info("Vendor folder detected, scanning for deps in path") - depPath, err = exec.LookPath("dep") - if err != nil { - // Do not halt build unless a new package needs to be imported - utils.Logger.Warn("Build: `dep` executable not found in PATH, but vendor folder detected." + - "Packages can only be added automatically to the vendor folder using the `dep` tool. " + - "You can install the `dep` tool by doing a `go get -u github.com/golang/dep/cmd/dep`") - } - } else { - utils.Logger.Info("No vendor folder detected, not using dependency manager to import files") - } - pkg, err := build.Default.Import(paths.ImportPath, "", build.FindOnly) if err != nil { - utils.Logger.Fatal("Failure importing", "path", paths.ImportPath) + return } // Binary path is a combination of $GOBIN/revel.d directory, app's import path and its name. @@ -191,6 +183,7 @@ func Build(c *model.CommandConfig, paths *model.RevelContainer) (app *App, compi buildCmd.Env = append(os.Environ(), "GOPATH="+gopath, ) + utils.CmdInit(buildCmd, c.AppPath) utils.Logger.Info("Exec:", "args", buildCmd.Args) output, err := buildCmd.CombinedOutput() @@ -220,32 +213,9 @@ func Build(c *model.CommandConfig, paths *model.RevelContainer) (app *App, compi return nil, newCompileError(paths, output) } gotten[pkgName] = struct{}{} - - // Execute "go get " - // Or dep `dep ensure -add ` if it is there - var getCmd *exec.Cmd - if useVendor { - if depPath == "" { - utils.Logger.Warn("Build: Vendor folder found, but the `dep` tool was not found, " + - "if you use a different vendoring (package management) tool please add the following packages by hand, " + - "or install the `dep` tool into your gopath by doing a `go get -u github.com/golang/dep/cmd/dep`. " + - "For more information and usage of the tool please see http://github.com/golang/dep") - for _, pkg := range matches { - utils.Logger.Warn("Missing package", "package", pkg[1]) - } - } - getCmd = exec.Command(depPath, "ensure", "-add", pkgName) - getCmd.Dir = paths.AppPath - - } else { - getCmd = exec.Command(goPath, "get", pkgName) - } - utils.Logger.Info("Exec:", "args", getCmd.Args) - getOutput, err := getCmd.CombinedOutput() - if err != nil { - utils.Logger.Error("Build failed", "message", stOutput, "error", err) - utils.Logger.Error("Failed to fetch the output", "getOutput", string(getOutput)) - return nil, newCompileError(paths, output) + 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())) } } @@ -336,12 +306,9 @@ func cleanDir(paths *model.RevelContainer, dir string) { // 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{}) { +func genSource(paths *model.RevelContainer, dir, filename, templateSource string, args map[string]interface{}) error { - err := utils.MustGenerateTemplate(filepath.Join(paths.AppPath, dir, filename), templateSource, args) - if err != nil { - utils.Logger.Fatal("Failed to generate template for source file", "error", err) - } + 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 @@ -433,7 +400,8 @@ func newCompileError(paths *model.RevelContainer, output []byte) *utils.Error { // 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, relFilename) + newPath := filepath.Join(gp,"src", paths.ImportPath, relFilename) + println(newPath) if utils.Exists(newPath) { return newPath } @@ -443,6 +411,7 @@ func newCompileError(paths *model.RevelContainer, output []byte) *utils.Error { return newPath } + // Read the source for the offending file. var ( relFilename = string(errorMatch[1]) // e.g. "src/revel/sample/app/controllers/app.go" @@ -467,7 +436,7 @@ func newCompileError(paths *model.RevelContainer, output []byte) *utils.Error { fileStr, err := utils.ReadLines(absFilename) if err != nil { compileError.MetaError = absFilename + ": " + err.Error() - utils.Logger.Error("Unable to readlines "+compileError.MetaError, "error", err) + utils.Logger.Info("Unable to readlines "+compileError.MetaError, "error", err) return compileError } diff --git a/harness/harness.go b/harness/harness.go index 63f8d97..1b00776 100644 --- a/harness/harness.go +++ b/harness/harness.go @@ -31,9 +31,9 @@ import ( "github.com/revel/cmd/model" "github.com/revel/cmd/utils" "github.com/revel/cmd/watcher" - "sync" "html/template" "io/ioutil" + "sync" ) var ( @@ -63,31 +63,31 @@ func (h *Harness) renderError(iw http.ResponseWriter, ir *http.Request, err erro // 1) Application/views/errors // 2) revel_home/views/errors // 3) views/errors - if err==nil { + if err == nil { utils.Logger.Panic("Caller passed in a nil error") } templateSet := template.New("__root__") - seekViewOnPath:=func(view string) (path string) { + seekViewOnPath := func(view string) (path string) { path = filepath.Join(h.paths.ViewsPath, "errors", view) if !utils.Exists(path) { path = filepath.Join(h.paths.RevelPath, "templates", "errors", view) } - data,err := ioutil.ReadFile(path) - if err!=nil { + data, err := ioutil.ReadFile(path) + if err != nil { utils.Logger.Error("Unable to read template file", path) } - _,err = templateSet.New("errors/"+view).Parse(string(data)) - if err!=nil { + _, err = templateSet.New("errors/" + view).Parse(string(data)) + if err != nil { utils.Logger.Error("Unable to parse template file", path) } return } - target := []string{seekViewOnPath("500.html"),seekViewOnPath("500-dev.html")} + target := []string{seekViewOnPath("500.html"), seekViewOnPath("500-dev.html")} if !utils.Exists(target[0]) { fmt.Fprintf(iw, "Target template not found not found %s
\n", target[0]) fmt.Fprintf(iw, "An error ocurred %s", err.Error()) - return + return } var revelError *utils.Error switch e := err.(type) { @@ -108,16 +108,11 @@ func (h *Harness) renderError(iw http.ResponseWriter, ir *http.Request, err erro viewArgs["DevMode"] = h.paths.DevMode viewArgs["Error"] = revelError - - // Render the template from the file - err = templateSet.ExecuteTemplate(iw,"errors/500.html",viewArgs) - if err!=nil { - utils.Logger.Error("Failed to execute","error",err) + err = templateSet.ExecuteTemplate(iw, "errors/500.html", viewArgs) + if err != nil { + utils.Logger.Error("Failed to execute", "error", err) } - fmt.Println("template ",templateSet.Templates()[0].Name(), templateSet.Templates()[1].Name()) - //utils.MustRenderTemplateToStream(iw,target, viewArgs) - } // ServeHTTP handles all requests. @@ -192,7 +187,6 @@ func NewHarness(c *model.CommandConfig, paths *model.RevelContainer, runMode str useProxy: !noProxy, config: c, runMode: runMode, - } if paths.HTTPSsl { @@ -219,9 +213,17 @@ func (h *Harness) Refresh() (err *utils.Error) { } utils.Logger.Info("Rebuild Called") - h.app, err = Build(h.config, h.paths) - if err != nil { - utils.Logger.Error("Build detected an error", "error", err) + var newErr error + h.app, newErr = Build(h.config, h.paths) + if newErr != nil { + utils.Logger.Error("Build detected an error", "error", newErr) + if castErr, ok := newErr.(*utils.Error); ok { + return castErr + } + err = &utils.Error{ + Title: "App failed to start up", + Description: err.Error(), + } return } @@ -269,7 +271,7 @@ func (h *Harness) Run() { if h.useProxy { go func() { // Check the port to start on a random port - if h.paths.HTTPPort==0 { + if h.paths.HTTPPort == 0 { h.paths.HTTPPort = getFreePort() } addr := fmt.Sprintf("%s:%d", h.paths.HTTPAddr, h.paths.HTTPPort) diff --git a/logger/doc.go b/logger/doc.go index 4392734..7cd8296 100644 --- a/logger/doc.go +++ b/logger/doc.go @@ -1,4 +1,3 @@ - /* Package logger contains filters and handles for the logging utilities in Revel. These facilities all currently use the logging library called log15 at @@ -7,5 +6,5 @@ Wrappers for the handlers are written here to provide a kind of isolation layer for Revel in case sometime in the future we would like to switch to another source to implement logging - */ +*/ package logger diff --git a/logger/logger.go b/logger/logger.go index a0417f1..8474d00 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -121,6 +121,7 @@ func SetDefaultLog(fromLog MultiLogger) { func (rl *RevelLogger) Debugf(msg string, param ...interface{}) { rl.Debug(fmt.Sprintf(msg, param...)) } + // Formatted info call func (rl *RevelLogger) Infof(msg string, param ...interface{}) { rl.Info(fmt.Sprintf(msg, param...)) diff --git a/logger/utils.go b/logger/utils.go index 3a98b8d..6d5dc87 100644 --- a/logger/utils.go +++ b/logger/utils.go @@ -1,9 +1,9 @@ package logger import ( - "gopkg.in/stack.v0" "github.com/revel/config" "github.com/revel/log15" + "gopkg.in/stack.v0" "log" "os" "path/filepath" @@ -43,15 +43,14 @@ func GetLogger(name string, logger MultiLogger) (l *log.Logger) { // Get all handlers based on the Config (if available) func InitializeFromConfig(basePath string, config *config.Context) (c *CompositeMultiHandler) { // If running in test mode suppress anything that is not an error - if config!=nil && config.BoolDefault("testModeFlag",false) { - config.SetOption("log.info.output","none") - config.SetOption("log.debug.output","none") - config.SetOption("log.warn.output","none") - config.SetOption("log.error.output","stderr") - config.SetOption("log.crit.output","stderr") + if config != nil && config.BoolDefault("testModeFlag", false) { + config.SetOption("log.info.output", "off") + config.SetOption("log.debug.output", "off") + config.SetOption("log.warn.output", "off") + config.SetOption("log.error.output", "stderr") + config.SetOption("log.crit.output", "stderr") } - // If the configuration has an all option we can skip some c, _ = NewCompositeMultiHandler() diff --git a/model/command_config.go b/model/command_config.go index e7f6ae6..3de36f9 100644 --- a/model/command_config.go +++ b/model/command_config.go @@ -2,11 +2,14 @@ package model // The constants import ( + "fmt" + "github.com/revel/cmd/logger" + "github.com/revel/cmd/utils" "go/build" "os" + "os/exec" "path/filepath" "strings" - "github.com/revel/cmd/utils" ) const ( @@ -25,58 +28,58 @@ type ( // The Command config for the line input CommandConfig struct { - Index COMMAND // The index - Verbose bool `short:"v" long:"debug" description:"If set the logger is set to verbose"` // True if debug is active - HistoricMode bool `long:"historic-run-mode" description:"If set the runmode is passed a string not json"` // True if debug is active - ImportPath string // The import path (converted from various commands) - GoPath string // The GoPath - GoCmd string // The full path to the go executable - SrcRoot string // The source root - AppPath string // The application path - AppName string // The applicaiton name - BasePath string // The base path - SkeletonPath string // The skeleton path - BuildFlags []string `short:"X" long:"build-flags" description:"These flags will be used when building the application. May be specified multiple times, only applicable for Build, Run, Package, Test commands"` + Index COMMAND // The index + Verbose []bool `short:"v" long:"debug" description:"If set the logger is set to verbose"` // True if debug is active + HistoricMode bool `long:"historic-run-mode" description:"If set the runmode is passed a string not json"` // True if debug is active + ImportPath string // The import path (relative to a GOPATH) + GoPath string // The GoPath + GoCmd string // The full path to the go executable + SrcRoot string // The source root + AppPath string // The application path (absolute) + AppName string // The application name + PackageResolver func(pkgName string) error // a packge resolver for the config + BuildFlags []string `short:"X" long:"build-flags" description:"These flags will be used when building the application. May be specified multiple times, only applicable for Build, Run, Package, Test commands"` // The new command New struct { - ImportPath string `short:"a" long:"application-path" description:"Path to applicaiton folder" required:"true"` - Skeleton string `short:"s" long:"skeleton" description:"Path to skeleton folder (Must exist on GO PATH)" required:"false"` - Vendored bool `short:"V" long:"vendor" description:"True if project should contain a vendor folder to be initialized. Creates the vendor folder and the 'Gopkg.toml' file in the root"` - Run bool `short:"r" long:"run" description:"True if you want to run the application right away"` + ImportPath string `short:"a" long:"application-path" description:"Path to application folder" required:"false"` + SkeletonPath string `short:"s" long:"skeleton" description:"Path to skeleton folder (Must exist on GO PATH)" required:"false"` + Vendored bool `short:"V" long:"vendor" description:"True if project should contain a vendor folder to be initialized. Creates the vendor folder and the 'Gopkg.toml' file in the root"` + Run bool `short:"r" long:"run" description:"True if you want to run the application right away"` } `command:"new"` // The build command Build struct { - TargetPath string `short:"t" long:"target-path" description:"Path to target folder. Folder will be completely deleted if it exists" required:"true"` - ImportPath string `short:"a" long:"application-path" description:"Path to applicaiton folder" required:"true"` + TargetPath string `short:"t" long:"target-path" description:"Path to target folder. Folder will be completely deleted if it exists" required:"false"` + ImportPath string `short:"a" long:"application-path" description:"Path to application folder" required:"false"` Mode string `short:"m" long:"run-mode" description:"The mode to run the application in"` CopySource bool `short:"s" long:"include-source" description:"Copy the source code as well"` } `command:"build"` // The run command Run struct { - ImportPath string `short:"a" long:"application-path" description:"Path to applicaiton folder" required:"true"` + ImportPath string `short:"a" long:"application-path" description:"Path to application folder" required:"false"` Mode string `short:"m" long:"run-mode" description:"The mode to run the application in"` - Port string `short:"p" long:"port" description:"The port to listen"` + Port int `short:"p" long:"port" default:"-1" description:"The port to listen" ` NoProxy bool `short:"n" long:"no-proxy" description:"True if proxy server should not be started. This will only update the main and routes files on change"` } `command:"run"` // The package command Package struct { + TargetPath string `short:"t" long:"target-path" description:"Full path and filename of target package to deploy" required:"false"` Mode string `short:"m" long:"run-mode" description:"The mode to run the application in"` - ImportPath string `short:"a" long:"application-path" description:"Path to applicaiton folder" required:"true"` + ImportPath string `short:"a" long:"application-path" description:"Path to application folder" required:"false"` CopySource bool `short:"s" long:"include-source" description:"Copy the source code as well"` } `command:"package"` // The clean command Clean struct { - ImportPath string `short:"a" long:"application-path" description:"Path to applicaiton folder" required:"true"` + ImportPath string `short:"a" long:"application-path" description:"Path to application folder" required:"false"` } `command:"clean"` // The test command Test struct { Mode string `short:"m" long:"run-mode" description:"The mode to run the application in"` - ImportPath string `short:"a" long:"application-path" description:"Path to applicaiton folder" required:"true"` + ImportPath string `short:"a" long:"application-path" description:"Path to application folder" required:"false"` Function string `short:"f" long:"suite-function" description:"The suite.function"` } `command:"test"` // The version command Version struct { - ImportPath string `short:"a" long:"application-path" description:"Path to applicaiton folder" required:"false"` + ImportPath string `short:"a" long:"application-path" description:"Path to application folder" required:"false"` } `command:"version"` } ) @@ -115,13 +118,14 @@ func (c *CommandConfig) UpdateImportPath() bool { currentPath, _ = filepath.Abs(importPath) } + if err == nil { for _, path := range strings.Split(build.Default.GOPATH, string(filepath.ListSeparator)) { utils.Logger.Infof("Checking import path %s with %s", currentPath, path) - if strings.HasPrefix(currentPath, path) { - importPath = currentPath[len(path) + 1:] + if strings.HasPrefix(currentPath, path) && len(currentPath) > len(path)+1 { + importPath = currentPath[len(path)+1:] // Remove the source from the path if it is there - if len(importPath)>4 && strings.ToLower(importPath[0:4]) == "src/" { + if len(importPath) > 4 && strings.ToLower(importPath[0:4]) == "src/" { importPath = importPath[4:] } else if importPath == "src" { importPath = "" @@ -133,6 +137,122 @@ func (c *CommandConfig) UpdateImportPath() bool { } c.ImportPath = importPath - utils.Logger.Info("Returned import path", "path", importPath, "buildpath",build.Default.GOPATH) + utils.Logger.Info("Returned import path", "path", importPath, "buildpath", build.Default.GOPATH) return (len(importPath) > 0 || !required) } + +// Used to initialize the package resolver +func (c *CommandConfig) InitPackageResolver() { + useVendor := utils.DirExists(filepath.Join(c.AppPath, "vendor")) + if c.Index == NEW && c.New.Vendored { + useVendor = true + } + utils.Logger.Info("InitPackageResolver", "useVendor", useVendor, "path", c.AppPath) + + var ( + depPath string + err error + ) + + if useVendor { + utils.Logger.Info("Vendor folder detected, scanning for deps in path") + depPath, err = exec.LookPath("dep") + if err != nil { + // Do not halt build unless a new package needs to be imported + utils.Logger.Fatal("Build: `dep` executable not found in PATH, but vendor folder detected." + + "Packages can only be added automatically to the vendor folder using the `dep` tool. " + + "You can install the `dep` tool by doing a `go get -u github.com/golang/dep/cmd/dep`") + } + } + + // This should get called when needed + c.PackageResolver = func(pkgName string) error { + //useVendor := utils.DirExists(filepath.Join(c.AppPath, "vendor")) + + var getCmd *exec.Cmd + utils.Logger.Info("Request for package ", "package", pkgName, "use vendor", useVendor) + if useVendor { + utils.Logger.Info("Using dependency manager to import package", "package", pkgName) + + if depPath == "" { + utils.Logger.Error("Build: Vendor folder found, but the `dep` tool was not found, " + + "if you use a different vendoring (package management) tool please add the following packages by hand, " + + "or install the `dep` tool into your gopath by doing a `go get -u github.com/golang/dep/cmd/dep`. " + + "For more information and usage of the tool please see http://github.com/golang/dep") + utils.Logger.Error("Missing package", "package", pkgName) + return fmt.Errorf("Missing package %s", pkgName) + } + getCmd = exec.Command(depPath, "ensure", "-add", pkgName) + } else { + utils.Logger.Info("No vendor folder detected, not using dependency manager to import package", "package", pkgName) + getCmd = exec.Command(c.GoCmd, "get", pkgName) + } + + utils.CmdInit(getCmd, c.AppPath) + utils.Logger.Info("Go get command ", "exec", getCmd.Path, "dir", getCmd.Dir, "args", getCmd.Args, "env", getCmd.Env, "package", pkgName) + output, err := getCmd.CombinedOutput() + if err != nil { + fmt.Printf("Error stack %v\n", logger.NewCallStack()) + utils.Logger.Error("Failed to import package", "error", err, "gopath", build.Default.GOPATH, "GO-ROOT", build.Default.GOROOT, "output", string(output)) + } + return err + } +} + +// lookup and set Go related variables +func (c *CommandConfig) InitGoPaths() { + // lookup go path + c.GoPath = build.Default.GOPATH + if c.GoPath == "" { + utils.Logger.Fatal("Abort: GOPATH environment variable is not set. " + + "Please refer to http://golang.org/doc/code.html to configure your Go environment.") + } + + // check for go executable + var err error + c.GoCmd, err = exec.LookPath("go") + if err != nil { + utils.Logger.Fatal("Go executable not found in PATH.") + } + + // revel/revel#1004 choose go path relative to current working directory + + // What we want to do is to add the import to the end of the + // gopath, and discover which import exists - If none exist this is an error except in the case + // where we are dealing with new which is a special case where we will attempt to target the working directory first + workingDir, _ := os.Getwd() + goPathList := filepath.SplitList(c.GoPath) + bestpath := "" + for _, path := range goPathList { + if c.Index == NEW { + // If the GOPATH is part of the working dir this is the most likely target + if strings.HasPrefix(workingDir, path) { + bestpath = path + } + } else { + if utils.Exists(filepath.Join(path, "src", c.ImportPath)) { + c.SrcRoot = path + break + } + } + } + + if len(c.SrcRoot)==0 && len(bestpath) > 0 { + c.SrcRoot = bestpath + } + utils.Logger.Info("Source root", "path", c.SrcRoot, "cwd", workingDir, "gopath", c.GoPath) + + // If source root is empty and this isn't a version then skip it + if len(c.SrcRoot) == 0 { + if c.Index != VERSION { + utils.Logger.Fatal("Abort: could not create a Revel application outside of GOPATH.") + } + return + } + + // set go src path + c.SrcRoot = filepath.Join(c.SrcRoot, "src") + + c.AppPath = filepath.Join(c.SrcRoot, filepath.FromSlash(c.ImportPath)) + utils.Logger.Info("Set application path", "path", c.AppPath) +} diff --git a/model/embedded_type_name.go b/model/embedded_type_name.go index 6a67056..f6d6172 100644 --- a/model/embedded_type_name.go +++ b/model/embedded_type_name.go @@ -8,4 +8,4 @@ type EmbeddedTypeName struct { // Convert the type to a properly formatted import line func (s *EmbeddedTypeName) String() string { return s.ImportPath + "." + s.StructName -} \ No newline at end of file +} diff --git a/model/event.go b/model/event.go new file mode 100644 index 0000000..7032443 --- /dev/null +++ b/model/event.go @@ -0,0 +1,62 @@ +package model + +type ( + // The event type + Event int + // The event response + EventResponse int + // The handler signature + EventHandler func(typeOf Event, value interface{}) (responseOf EventResponse) + RevelCallback interface { + FireEvent(key Event, value interface{}) (response EventResponse) + PackageResolver(pkgName string) error + } +) + +const ( + // Event type when templates are going to be refreshed (receivers are registered template engines added to the template.engine conf option) + TEMPLATE_REFRESH_REQUESTED Event = iota + // Event type when templates are refreshed (receivers are registered template engines added to the template.engine conf option) + TEMPLATE_REFRESH_COMPLETED + // Event type before all module loads, events thrown to handlers added to AddInitEventHandler + + // Event type before all module loads, events thrown to handlers added to AddInitEventHandler + REVEL_BEFORE_MODULES_LOADED + // Event type before module loads, events thrown to handlers added to AddInitEventHandler + REVEL_BEFORE_MODULE_LOADED + // Event type after module loads, events thrown to handlers added to AddInitEventHandler + REVEL_AFTER_MODULE_LOADED + // Event type after all module loads, events thrown to handlers added to AddInitEventHandler + REVEL_AFTER_MODULES_LOADED + + // Event type before server engine is initialized, receivers are active server engine and handlers added to AddInitEventHandler + ENGINE_BEFORE_INITIALIZED + // Event type before server engine is started, receivers are active server engine and handlers added to AddInitEventHandler + ENGINE_STARTED + // Event type after server engine is stopped, receivers are active server engine and handlers added to AddInitEventHandler + ENGINE_SHUTDOWN + + // Called before routes are refreshed + ROUTE_REFRESH_REQUESTED + // Called after routes have been refreshed + ROUTE_REFRESH_COMPLETED + + // Fired when a panic is caught during the startup process + REVEL_FAILURE +) + +var initEventList = []EventHandler{} // Event handler list for receiving events + +// Fires system events from revel +func RaiseEvent(key Event, value interface{}) (response EventResponse) { + for _, handler := range initEventList { + response |= handler(key, value) + } + return +} + +// Add event handler to listen for all system events +func AddInitEventHandler(handler EventHandler) { + initEventList = append(initEventList, handler) + return +} diff --git a/model/event_test.go b/model/event_test.go new file mode 100644 index 0000000..e3f063c --- /dev/null +++ b/model/event_test.go @@ -0,0 +1,24 @@ +package model_test + +import ( + "github.com/revel/revel" + "github.com/stretchr/testify/assert" + "testing" +) + +// Test that the event handler can be attached and it dispatches the event received +func TestEventHandler(t *testing.T) { + counter := 0 + newListener := func(typeOf revel.Event, value interface{}) (responseOf revel.EventResponse) { + if typeOf == revel.REVEL_FAILURE { + counter++ + } + return + } + // Attach the same handlder twice so we expect to see the response twice as well + revel.AddInitEventHandler(newListener) + revel.AddInitEventHandler(newListener) + revel.RaiseEvent(revel.REVEL_AFTER_MODULES_LOADED, nil) + revel.RaiseEvent(revel.REVEL_FAILURE, nil) + assert.Equal(t, counter, 2, "Expected event handler to have been called") +} diff --git a/model/method.go b/model/method.go index 5652a8c..ea5357d 100644 --- a/model/method.go +++ b/model/method.go @@ -1,6 +1,5 @@ package model - // methodCall describes a call to c.Render(..) // It documents the argument names used, in order to propagate them to RenderArgs. type MethodCall struct { @@ -22,4 +21,3 @@ type MethodArg struct { TypeExpr TypeExpr // The name of the type, e.g. "int", "*pkg.UserType" ImportPath string // If the arg is of an imported type, this is the import path. } - diff --git a/model/revel_container.go b/model/revel_container.go index 88583fa..64ad4f3 100644 --- a/model/revel_container.go +++ b/model/revel_container.go @@ -6,92 +6,96 @@ import ( "github.com/revel/config" "go/build" - "os" + "errors" + "fmt" "path/filepath" "sort" "strings" ) -const ( - // Event type when templates are going to be refreshed (receivers are registered template engines added to the template.engine conf option) - TEMPLATE_REFRESH_REQUESTED = iota - // Event type when templates are refreshed (receivers are registered template engines added to the template.engine conf option) - TEMPLATE_REFRESH_COMPLETED - // Event type before all module loads, events thrown to handlers added to AddInitEventHandler - // Event type before all module loads, events thrown to handlers added to AddInitEventHandler - REVEL_BEFORE_MODULES_LOADED - // Event type called when a new module is found - REVEL_BEFORE_MODULE_LOADED - // Event type called when after a new module is found - REVEL_AFTER_MODULE_LOADED - // Event type after all module loads, events thrown to handlers added to AddInitEventHandler - REVEL_AFTER_MODULES_LOADED - - // Event type before server engine is initialized, receivers are active server engine and handlers added to AddInitEventHandler - ENGINE_BEFORE_INITIALIZED - // Event type before server engine is started, receivers are active server engine and handlers added to AddInitEventHandler - ENGINE_STARTED - // Event type after server engine is stopped, receivers are active server engine and handlers added to AddInitEventHandler - ENGINE_SHUTDOWN - - // Called before routes are refreshed - ROUTE_REFRESH_REQUESTED - // Called after routes have been refreshed - ROUTE_REFRESH_COMPLETED -) type ( // The container object for describing all Revels variables RevelContainer struct { - ImportPath string // The import path - SourcePath string // The full source path - RunMode string // The current run mode - RevelPath string // The path to the Revel source code - BasePath string // The base path to the application - AppPath string // The application path (BasePath + "/app" - ViewsPath string // The application views path - CodePaths []string // All the code paths - TemplatePaths []string // All the template paths - ConfPaths []string // All the configuration paths - Config *config.Context // The global config object - Packaged bool // True if packaged - DevMode bool // True if running in dev mode - HTTPPort int // The http port - HTTPAddr string // The http address - HTTPSsl bool // True if running https - HTTPSslCert string // The SSL certificate - HTTPSslKey string // The SSL key - AppName string // The application name - AppRoot string // The application root from the config `app.root` - CookiePrefix string // The cookie prefix - CookieDomain string // The cookie domain - CookieSecure bool // True if cookie is secure - SecretStr string // The secret string - MimeConfig *config.Context // The mime configuration + BuildPaths struct { + Revel string + } + Paths struct { + Import string + Source string + Base string + App string + Views string + Code []string + Template []string + Config []string + } + PackageInfo struct { + Config config.Context + Packaged bool + DevMode bool + Vendor bool + } + Application struct { + Name string + Root string + } + + ImportPath string // The import path + SourcePath string // The full source path + RunMode string // The current run mode + RevelPath string // The path to the Revel source code + BasePath string // The base path to the application + AppPath string // The application path (BasePath + "/app") + ViewsPath string // The application views path + CodePaths []string // All the code paths + TemplatePaths []string // All the template paths + ConfPaths []string // All the configuration paths + Config *config.Context // The global config object + Packaged bool // True if packaged + DevMode bool // True if running in dev mode + HTTPPort int // The http port + HTTPAddr string // The http address + HTTPSsl bool // True if running https + HTTPSslCert string // The SSL certificate + HTTPSslKey string // The SSL key + AppName string // The application name + AppRoot string // The application root from the config `app.root` + CookiePrefix string // The cookie prefix + CookieDomain string // The cookie domain + CookieSecure bool // True if cookie is secure + SecretStr string // The secret string + MimeConfig *config.Context // The mime configuration ModulePathMap map[string]string // The module path map } - RevelCallback interface { - FireEvent(key int, value interface{}) (response int) + WrappedRevelCallback struct { + FireEventFunction func(key Event, value interface{}) (response EventResponse) + ImportFunction func(pkgName string) error } - doNothingRevelCallback struct { - - } - ) -// Simple callback to pass to the RevelCallback that does nothing -var DoNothingRevelCallback = RevelCallback(&doNothingRevelCallback{}) +// Simple Wrapped RevelCallback +func NewWrappedRevelCallback(fe func(key Event, value interface{}) (response EventResponse), ie func(pkgName string) error) RevelCallback { + return &WrappedRevelCallback{fe, ie} +} -func (_ *doNothingRevelCallback) FireEvent(key int, value interface{}) (response int) { +// Function to implement the FireEvent +func (w *WrappedRevelCallback) FireEvent(key Event, value interface{}) (response EventResponse) { + if w.FireEventFunction != nil { + response = w.FireEventFunction(key, value) + } return } +func (w *WrappedRevelCallback) PackageResolver(pkgName string) error { + return w.ImportFunction(pkgName) +} // RevelImportPath Revel framework import path var RevelImportPath = "github.com/revel/revel" // This function returns a container object describing the revel application // eventually this type of function will replace the global variables. -func NewRevelPaths(mode, importPath, srcPath string, callback RevelCallback) (rp *RevelContainer) { +func NewRevelPaths(mode, importPath, srcPath string, callback RevelCallback) (rp *RevelContainer, err error) { rp = &RevelContainer{ModulePathMap: map[string]string{}} // Ignore trailing slashes. rp.ImportPath = strings.TrimRight(importPath, "/") @@ -101,17 +105,20 @@ func NewRevelPaths(mode, importPath, srcPath string, callback RevelCallback) (rp // If the SourcePath is not specified, find it using build.Import. var revelSourcePath string // may be different from the app source path if rp.SourcePath == "" { - revelSourcePath, rp.SourcePath = findSrcPaths(importPath) + rp.SourcePath, revelSourcePath, err = utils.FindSrcPaths(importPath, RevelImportPath, callback.PackageResolver) + if err != nil { + return + } } else { // If the SourcePath was specified, assume both Revel and the app are within it. rp.SourcePath = filepath.Clean(rp.SourcePath) revelSourcePath = rp.SourcePath - } // Setup paths for application rp.RevelPath = filepath.Join(revelSourcePath, filepath.FromSlash(RevelImportPath)) rp.BasePath = filepath.Join(rp.SourcePath, filepath.FromSlash(importPath)) + rp.PackageInfo.Vendor = utils.Exists(filepath.Join(rp.BasePath, "vendor")) rp.AppPath = filepath.Join(rp.BasePath, "app") rp.ViewsPath = filepath.Join(rp.AppPath, "views") @@ -133,11 +140,9 @@ func NewRevelPaths(mode, importPath, srcPath string, callback RevelCallback) (rp }, rp.ConfPaths...) - var err error rp.Config, err = config.LoadContext("app.conf", rp.ConfPaths) if err != nil { - utils.Logger.Fatal("Unable to load configuartion file ","error", err) - os.Exit(1) + return rp, fmt.Errorf("Unable to load configuartion file %s", err) } // Ensure that the selected runmode appears in app.conf. @@ -146,7 +151,7 @@ func NewRevelPaths(mode, importPath, srcPath string, callback RevelCallback) (rp mode = config.DefaultSection } if !rp.Config.HasSection(mode) { - utils.Logger.Fatal("app.conf: No mode found:","run-mode", mode) + return rp, fmt.Errorf("app.conf: No mode found: %s %s", "run-mode", mode) } rp.Config.SetSection(mode) @@ -159,10 +164,10 @@ func NewRevelPaths(mode, importPath, srcPath string, callback RevelCallback) (rp rp.HTTPSslKey = rp.Config.StringDefault("http.sslkey", "") if rp.HTTPSsl { if rp.HTTPSslCert == "" { - utils.Logger.Fatal("No http.sslcert provided.") + return rp, errors.New("No http.sslcert provided.") } if rp.HTTPSslKey == "" { - utils.Logger.Fatal("No http.sslkey provided.") + return rp, errors.New("No http.sslkey provided.") } } // @@ -173,21 +178,23 @@ func NewRevelPaths(mode, importPath, srcPath string, callback RevelCallback) (rp rp.CookieSecure = rp.Config.BoolDefault("cookie.secure", rp.HTTPSsl) rp.SecretStr = rp.Config.StringDefault("app.secret", "") - callback.FireEvent(REVEL_BEFORE_MODULES_LOADED, nil) - rp.loadModules(callback) + if err := rp.loadModules(callback); err != nil { + return rp, err + } + callback.FireEvent(REVEL_AFTER_MODULES_LOADED, nil) return } // LoadMimeConfig load mime-types.conf on init. -func (rp *RevelContainer) LoadMimeConfig() { - var err error +func (rp *RevelContainer) LoadMimeConfig() (err error) { rp.MimeConfig, err = config.LoadContext("mime-types.conf", rp.ConfPaths) if err != nil { - utils.Logger.Fatal("Failed to load mime type config:", "error", err) + return fmt.Errorf("Failed to load mime type config: %s %s", "error", err) } + return } // Loads modules based on the configuration setup. @@ -195,7 +202,7 @@ func (rp *RevelContainer) LoadMimeConfig() { // for each module loaded. The callback will receive the RevelContainer, name, moduleImportPath and modulePath // It will automatically add in the code paths for the module to the // container object -func (rp *RevelContainer) loadModules(callback RevelCallback) { +func (rp *RevelContainer) loadModules(callback RevelCallback) (err error) { keys := []string{} for _, key := range rp.Config.Options("module.") { keys = append(keys, key) @@ -211,7 +218,12 @@ func (rp *RevelContainer) loadModules(callback RevelCallback) { modulePath, err := rp.ResolveImportPath(moduleImportPath) if err != nil { - utils.Logger.Error("Failed to load module. Import of path failed", "modulePath", moduleImportPath, "error", err) + utils.Logger.Info("Missing module ", "module", moduleImportPath, "error",err) + callback.PackageResolver(moduleImportPath) + modulePath, err = rp.ResolveImportPath(moduleImportPath) + if err != nil { + return fmt.Errorf("Failed to load module. Import of path failed %s:%s %s:%s ", "modulePath", moduleImportPath, "error", err) + } } // Drop anything between module.???. name := key[len("module."):] @@ -222,6 +234,7 @@ func (rp *RevelContainer) loadModules(callback RevelCallback) { rp.addModulePaths(name, moduleImportPath, modulePath) callback.FireEvent(REVEL_AFTER_MODULE_LOADED, []interface{}{rp, name, moduleImportPath, modulePath}) } + return } // Adds a module paths to the container object @@ -252,43 +265,12 @@ func (rp *RevelContainer) ResolveImportPath(importPath string) (string, error) { return filepath.Join(rp.SourcePath, importPath), nil } - modPkg, err := build.Import(importPath, rp.RevelPath, build.FindOnly) + modPkg, err := build.Import(importPath, rp.AppPath, build.FindOnly) if err != nil { return "", err } + if rp.PackageInfo.Vendor && !strings.HasPrefix(modPkg.Dir,rp.BasePath) { + return "", fmt.Errorf("Module %s was found outside of path %s.",importPath, modPkg.Dir) + } return modPkg.Dir, nil } - -// Find the full source dir for the import path, uses the build.Default.GOPATH to search for the directory -func findSrcPaths(importPath string) (revelSourcePath, appSourcePath string) { - var ( - gopaths = filepath.SplitList(build.Default.GOPATH) - goroot = build.Default.GOROOT - ) - - if len(gopaths) == 0 { - utils.Logger.Fatalf("GOPATH environment variable is not set. " + - "Please refer to http://golang.org/doc/code.html to configure your Go environment.") - } - - if utils.ContainsString(gopaths, goroot) { - utils.Logger.Fatalf("GOPATH (%s) must not include your GOROOT (%s). "+ - "Please refer to http://golang.org/doc/code.html to configure your Go environment.", - gopaths, goroot) - - } - - appPkg, err := build.Import(importPath, "", build.FindOnly) - if err != nil { - utils.Logger.Fatal("Failed to import "+importPath+" with error:", "error", err) - } - - revelPkg, err := build.Import(RevelImportPath, appPkg.Dir, build.FindOnly) - if err != nil { - utils.Logger.Fatal("Failed to find Revel with error:", "error", err) - } - - revelSourcePath, appSourcePath = revelPkg.Dir[:len(revelPkg.Dir)-len(RevelImportPath)], appPkg.SrcRoot - return -} - diff --git a/model/type_info.go b/model/type_info.go index 5489992..97fb6bb 100644 --- a/model/type_info.go +++ b/model/type_info.go @@ -2,10 +2,10 @@ package model // TypeInfo summarizes information about a struct type in the app source code. type TypeInfo struct { - StructName string // e.g. "Application" - ImportPath string // e.g. "github.com/revel/examples/chat/app/controllers" - PackageName string // e.g. "controllers" - MethodSpecs []*MethodSpec // Method specifications, the action functions + StructName string // e.g. "Application" + ImportPath string // e.g. "github.com/revel/examples/chat/app/controllers" + PackageName string // e.g. "controllers" + MethodSpecs []*MethodSpec // Method specifications, the action functions EmbeddedTypes []*EmbeddedTypeName // Used internally to identify controllers that indirectly embed *revel.Controller. } @@ -13,4 +13,3 @@ type TypeInfo struct { func (s *TypeInfo) String() string { return s.ImportPath + "." + s.StructName } - diff --git a/parser/appends.go b/parser/appends.go new file mode 100644 index 0000000..2990795 --- /dev/null +++ b/parser/appends.go @@ -0,0 +1,223 @@ +package parser + +import ( + "go/ast" + "github.com/revel/cmd/utils" + "github.com/revel/cmd/model" + "go/token" +) + +// If this Decl is a struct type definition, it is summarized and added to specs. +// Else, specs is returned unchanged. +func appendStruct(fileName string, specs []*model.TypeInfo, pkgImportPath string, pkg *ast.Package, decl ast.Decl, imports map[string]string, fset *token.FileSet) []*model.TypeInfo { + // Filter out non-Struct type declarations. + spec, found := getStructTypeDecl(decl, fset) + if !found { + return specs + } + + structType := spec.Type.(*ast.StructType) + + // At this point we know it's a type declaration for a struct. + // Fill in the rest of the info by diving into the fields. + // Add it provisionally to the Controller list -- it's later filtered using field info. + controllerSpec := &model.TypeInfo{ + StructName: spec.Name.Name, + ImportPath: pkgImportPath, + PackageName: pkg.Name, + } + + for _, field := range structType.Fields.List { + // If field.Names is set, it's not an embedded type. + if field.Names != nil { + continue + } + + // A direct "sub-type" has an ast.Field as either: + // Ident { "AppController" } + // SelectorExpr { "rev", "Controller" } + // Additionally, that can be wrapped by StarExprs. + fieldType := field.Type + pkgName, typeName := func() (string, string) { + // Drill through any StarExprs. + for { + if starExpr, ok := fieldType.(*ast.StarExpr); ok { + fieldType = starExpr.X + continue + } + break + } + + // If the embedded type is in the same package, it's an Ident. + if ident, ok := fieldType.(*ast.Ident); ok { + return "", ident.Name + } + + if selectorExpr, ok := fieldType.(*ast.SelectorExpr); ok { + if pkgIdent, ok := selectorExpr.X.(*ast.Ident); ok { + return pkgIdent.Name, selectorExpr.Sel.Name + } + } + return "", "" + }() + + // If a typename wasn't found, skip it. + if typeName == "" { + continue + } + + // Find the import path for this type. + // If it was referenced without a package name, use the current package import path. + // Else, look up the package's import path by name. + var importPath string + if pkgName == "" { + importPath = pkgImportPath + } else { + var ok bool + if importPath, ok = imports[pkgName]; !ok { + utils.Logger.Error("Error: Failed to find import path for ", "package", pkgName, "type", typeName) + continue + } + } + + controllerSpec.EmbeddedTypes = append(controllerSpec.EmbeddedTypes, &model.EmbeddedTypeName{ + ImportPath: importPath, + StructName: typeName, + }) + } + + return append(specs, controllerSpec) +} + +// If decl is a Method declaration, it is summarized and added to the array +// underneath its receiver type. +// e.g. "Login" => {MethodSpec, MethodSpec, ..} +func appendAction(fset *token.FileSet, mm methodMap, decl ast.Decl, pkgImportPath, pkgName string, imports map[string]string) { + // Func declaration? + funcDecl, ok := decl.(*ast.FuncDecl) + if !ok { + return + } + + // Have a receiver? + if funcDecl.Recv == nil { + return + } + + // Is it public? + if !funcDecl.Name.IsExported() { + return + } + + // Does it return a Result? + if funcDecl.Type.Results == nil || len(funcDecl.Type.Results.List) != 1 { + return + } + selExpr, ok := funcDecl.Type.Results.List[0].Type.(*ast.SelectorExpr) + if !ok { + return + } + if selExpr.Sel.Name != "Result" { + return + } + if pkgIdent, ok := selExpr.X.(*ast.Ident); !ok || imports[pkgIdent.Name] != model.RevelImportPath { + return + } + + method := &model.MethodSpec{ + Name: funcDecl.Name.Name, + } + + // Add a description of the arguments to the method. + for _, field := range funcDecl.Type.Params.List { + for _, name := range field.Names { + var importPath string + typeExpr := model.NewTypeExprFromAst(pkgName, field.Type) + if !typeExpr.Valid { + utils.Logger.Warn("Warn: Didn't understand argument '%s' of action %s. Ignoring.", name, getFuncName(funcDecl)) + return // We didn't understand one of the args. Ignore this action. + } + // Local object + if typeExpr.PkgName == pkgName { + importPath = pkgImportPath + } else if typeExpr.PkgName != "" { + var ok bool + if importPath, ok = imports[typeExpr.PkgName]; !ok { + utils.Logger.Fatalf("Failed to find import for arg of type: %s , %s", typeExpr.PkgName, typeExpr.TypeName("")) + } + } + method.Args = append(method.Args, &model.MethodArg{ + Name: name.Name, + TypeExpr: typeExpr, + ImportPath: importPath, + }) + } + } + + // Add a description of the calls to Render from the method. + // Inspect every node (e.g. always return true). + method.RenderCalls = []*model.MethodCall{} + ast.Inspect(funcDecl.Body, func(node ast.Node) bool { + // Is it a function call? + callExpr, ok := node.(*ast.CallExpr) + if !ok { + return true + } + + // Is it calling (*Controller).Render? + selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + + // The type of the receiver is not easily available, so just store every + // call to any method called Render. + if selExpr.Sel.Name != "Render" { + return true + } + + // Add this call's args to the renderArgs. + pos := fset.Position(callExpr.Lparen) + methodCall := &model.MethodCall{ + Line: pos.Line, + Names: []string{}, + } + for _, arg := range callExpr.Args { + argIdent, ok := arg.(*ast.Ident) + if !ok { + continue + } + methodCall.Names = append(methodCall.Names, argIdent.Name) + } + method.RenderCalls = append(method.RenderCalls, methodCall) + return true + }) + + var recvTypeName string + var recvType = funcDecl.Recv.List[0].Type + if recvStarType, ok := recvType.(*ast.StarExpr); ok { + recvTypeName = recvStarType.X.(*ast.Ident).Name + } else { + recvTypeName = recvType.(*ast.Ident).Name + } + + mm[recvTypeName] = append(mm[recvTypeName], method) +} + +// Combine the 2 source info models into one +func appendSourceInfo(srcInfo1, srcInfo2 *model.SourceInfo) *model.SourceInfo { + if srcInfo1 == nil { + return srcInfo2 + } + + srcInfo1.StructSpecs = append(srcInfo1.StructSpecs, srcInfo2.StructSpecs...) + srcInfo1.InitImportPaths = append(srcInfo1.InitImportPaths, srcInfo2.InitImportPaths...) + for k, v := range srcInfo2.ValidationKeys { + if _, ok := srcInfo1.ValidationKeys[k]; ok { + utils.Logger.Warn("Warn: Key conflict when scanning validation calls:", "key", k) + continue + } + srcInfo1.ValidationKeys[k] = v + } + return srcInfo1 +} diff --git a/parser/imports.go b/parser/imports.go new file mode 100644 index 0000000..d0edd15 --- /dev/null +++ b/parser/imports.go @@ -0,0 +1,82 @@ +package parser + +import ( + "github.com/revel/cmd/utils" + "go/ast" + "go/build" + "go/token" + "path/filepath" + "strings" +) + +// Add imports to the map from the source dir +func addImports(imports map[string]string, decl ast.Decl, srcDir string) { + genDecl, ok := decl.(*ast.GenDecl) + if !ok { + return + } + + if genDecl.Tok != token.IMPORT { + return + } + + for _, spec := range genDecl.Specs { + importSpec := spec.(*ast.ImportSpec) + var pkgAlias string + if importSpec.Name != nil { + pkgAlias = importSpec.Name.Name + if pkgAlias == "_" { + continue + } + } + quotedPath := importSpec.Path.Value // e.g. "\"sample/app/models\"" + fullPath := quotedPath[1 : len(quotedPath)-1] // Remove the quotes + + // If the package was not aliased (common case), we have to import it + // to see what the package name is. + // TODO: Can improve performance here a lot: + // 1. Do not import everything over and over again. Keep a cache. + // 2. Exempt the standard library; their directories always match the package name. + // 3. Can use build.FindOnly and then use parser.ParseDir with mode PackageClauseOnly + if pkgAlias == "" { + utils.Logger.Debug("Reading from build", "path", fullPath, "srcPath", srcDir, "gopath", build.Default.GOPATH) + pkg, err := build.Import(fullPath, srcDir, 0) + if err != nil { + // We expect this to happen for apps using reverse routing (since we + // have not yet generated the routes). Don't log that. + if !strings.HasSuffix(fullPath, "/app/routes") { + utils.Logger.Error("Could not find import:", "path", fullPath, "srcPath", srcDir, "error", err) + } + continue + } else { + utils.Logger.Debug("Found package in dir", "dir", pkg.Dir, "name", pkg.ImportPath) + } + pkgAlias = pkg.Name + } + + imports[pkgAlias] = fullPath + } +} + +// Returns a valid import string from the path +// using the build.Defaul.GOPATH to determine the root +func importPathFromPath(root string) string { + if vendorIdx := strings.Index(root, "/vendor/"); vendorIdx != -1 { + return filepath.ToSlash(root[vendorIdx+8:]) + } + for _, gopath := range filepath.SplitList(build.Default.GOPATH) { + srcPath := filepath.Join(gopath, "src") + if strings.HasPrefix(root, srcPath) { + return filepath.ToSlash(root[len(srcPath)+1:]) + } + } + + srcPath := filepath.Join(build.Default.GOROOT, "src", "pkg") + if strings.HasPrefix(root, srcPath) { + utils.Logger.Warn("Code path should be in GOPATH, but is in GOROOT:", "path", root) + return filepath.ToSlash(root[len(srcPath)+1:]) + } + + utils.Logger.Error("Unexpected! Code path is not in GOPATH:", "path", root) + return "" +} diff --git a/parser/reflect.go b/parser/reflect.go index e143e2c..98caa06 100644 --- a/parser/reflect.go +++ b/parser/reflect.go @@ -9,7 +9,6 @@ package parser import ( "go/ast" - "go/build" "go/parser" "go/scanner" "go/token" @@ -21,6 +20,13 @@ import ( "github.com/revel/cmd/utils" ) +// A container used to support the reflection package +type processContainer struct { + root, rootImportPath string // The paths + paths *model.RevelContainer // The Revel paths + srcInfo *model.SourceInfo // The source information container +} + // Maps a controller simple name (e.g. "Login") to the methods for which it is a // receiver. type methodMap map[string][]*model.MethodSpec @@ -28,130 +34,116 @@ type methodMap map[string][]*model.MethodSpec // ProcessSource parses the app controllers directory and // returns a list of the controller types found. // Otherwise CompileError if the parsing fails. -func ProcessSource(paths *model.RevelContainer) (*model.SourceInfo, *utils.Error) { - var ( - srcInfo *model.SourceInfo - compileError *utils.Error - ) - +func ProcessSource(paths *model.RevelContainer) (_ *model.SourceInfo, compileError error) { + pc := &processContainer{paths: paths} for _, root := range paths.CodePaths { rootImportPath := importPathFromPath(root) if rootImportPath == "" { utils.Logger.Info("Skipping empty code path", "path", root) continue } + pc.root, pc.rootImportPath = root, rootImportPath // Start walking the directory tree. - _ = utils.Walk(root, func(path string, info os.FileInfo, err error) error { - if err != nil { - utils.Logger.Error("Error scanning app source:", "error", err) - return nil - } - - if !info.IsDir() || info.Name() == "tmp" { - return nil - } - - // Get the import path of the package. - pkgImportPath := rootImportPath - if root != path { - pkgImportPath = rootImportPath + "/" + filepath.ToSlash(path[len(root)+1:]) - } - - // Parse files within the path. - var pkgs map[string]*ast.Package - fset := token.NewFileSet() - pkgs, err = parser.ParseDir(fset, path, func(f os.FileInfo) bool { - return !f.IsDir() && !strings.HasPrefix(f.Name(), ".") && strings.HasSuffix(f.Name(), ".go") - }, 0) - if err != nil { - if errList, ok := err.(scanner.ErrorList); ok { - var pos = errList[0].Pos - compileError = &utils.Error{ - SourceType: ".go source", - Title: "Go Compilation Error", - Path: pos.Filename, - Description: errList[0].Msg, - Line: pos.Line, - Column: pos.Column, - SourceLines: utils.MustReadLines(pos.Filename), - } - - errorLink := paths.Config.StringDefault("error.link", "") - - if errorLink != "" { - compileError.SetLink(errorLink) - } - - return compileError - } - - // This is exception, err already checked above. Here just a print - ast.Print(nil, err) - utils.Logger.Fatal("Failed to parse dir", "error", err) - } - - // Skip "main" packages. - delete(pkgs, "main") - - // Ignore packages that end with _test - // These cannot be included in source code that is not generated specifically as a test - for i := range pkgs { - if len(i) > 6 { - if string(i[len(i)-5:]) == "_test" { - delete(pkgs, i) - } - } - } - - // If there is no code in this directory, skip it. - if len(pkgs) == 0 { - return nil - } - - // There should be only one package in this directory. - if len(pkgs) > 1 { - for i := range pkgs { - println("Found package ", i) - } - utils.Logger.Fatal("Most unexpected! Multiple packages in a single directory:", "packages", pkgs) - } - - - var pkg *ast.Package - for _, v := range pkgs { - pkg = v - } - - if pkg != nil { - srcInfo = appendSourceInfo(srcInfo, processPackage(fset, pkgImportPath, path, pkg)) - } else { - utils.Logger.Info("Ignoring package, because it contained no packages", "path", path) - } - return nil - }) + compileError = utils.Walk(root, pc.processPath) } - return srcInfo, compileError + return pc.srcInfo, compileError } -func appendSourceInfo(srcInfo1, srcInfo2 *model.SourceInfo) *model.SourceInfo { - if srcInfo1 == nil { - return srcInfo2 +// Called during the "walk process" +func (pc *processContainer) processPath(path string, info os.FileInfo, err error) error { + if err != nil { + utils.Logger.Error("Error scanning app source:", "error", err) + return nil } - srcInfo1.StructSpecs = append(srcInfo1.StructSpecs, srcInfo2.StructSpecs...) - srcInfo1.InitImportPaths = append(srcInfo1.InitImportPaths, srcInfo2.InitImportPaths...) - for k, v := range srcInfo2.ValidationKeys { - if _, ok := srcInfo1.ValidationKeys[k]; ok { - utils.Logger.Warn("Warn: Key conflict when scanning validation calls:", "key", k) - continue + if !info.IsDir() || info.Name() == "tmp" { + return nil + } + + // Get the import path of the package. + pkgImportPath := pc.rootImportPath + if pc.root != path { + pkgImportPath = pc.rootImportPath + "/" + filepath.ToSlash(path[len(pc.root)+1:]) + } + + // Parse files within the path. + var pkgs map[string]*ast.Package + fset := token.NewFileSet() + pkgs, err = parser.ParseDir( + fset, + path, + func(f os.FileInfo) bool { + return !f.IsDir() && !strings.HasPrefix(f.Name(), ".") && strings.HasSuffix(f.Name(), ".go") + }, + 0) + + if err != nil { + if errList, ok := err.(scanner.ErrorList); ok { + var pos = errList[0].Pos + newError := &utils.Error{ + SourceType: ".go source", + Title: "Go Compilation Error", + Path: pos.Filename, + Description: errList[0].Msg, + Line: pos.Line, + Column: pos.Column, + SourceLines: utils.MustReadLines(pos.Filename), + } + + errorLink := pc.paths.Config.StringDefault("error.link", "") + if errorLink != "" { + newError.SetLink(errorLink) + } + return newError } - srcInfo1.ValidationKeys[k] = v + + // This is exception, err already checked above. Here just a print + ast.Print(nil, err) + utils.Logger.Fatal("Failed to parse dir", "error", err) } - return srcInfo1 + + // Skip "main" packages. + delete(pkgs, "main") + + // Ignore packages that end with _test + // These cannot be included in source code that is not generated specifically as a test + for i := range pkgs { + if len(i) > 6 { + if string(i[len(i)-5:]) == "_test" { + delete(pkgs, i) + } + } + } + + // If there is no code in this directory, skip it. + if len(pkgs) == 0 { + return nil + } + + // There should be only one package in this directory. + if len(pkgs) > 1 { + for i := range pkgs { + println("Found package ", i) + } + utils.Logger.Fatal("Most unexpected! Multiple packages in a single directory:", "packages", pkgs) + } + + var pkg *ast.Package + for _, v := range pkgs { + pkg = v + } + + if pkg != nil { + pc.srcInfo = appendSourceInfo(pc.srcInfo, processPackage(fset, pkgImportPath, path, pkg)) + } else { + utils.Logger.Info("Ignoring package, because it contained no packages", "path", path) + } + return nil } +// Process a single package within a file func processPackage(fset *token.FileSet, pkgImportPath, pkgPath string, pkg *ast.Package) *model.SourceInfo { var ( structSpecs []*model.TypeInfo @@ -228,355 +220,6 @@ func getFuncName(funcDecl *ast.FuncDecl) string { return prefix + funcDecl.Name.Name } -func addImports(imports map[string]string, decl ast.Decl, srcDir string) { - genDecl, ok := decl.(*ast.GenDecl) - if !ok { - return - } - - if genDecl.Tok != token.IMPORT { - return - } - - for _, spec := range genDecl.Specs { - importSpec := spec.(*ast.ImportSpec) - var pkgAlias string - if importSpec.Name != nil { - pkgAlias = importSpec.Name.Name - if pkgAlias == "_" { - continue - } - } - quotedPath := importSpec.Path.Value // e.g. "\"sample/app/models\"" - fullPath := quotedPath[1 : len(quotedPath)-1] // Remove the quotes - - // If the package was not aliased (common case), we have to import it - // to see what the package name is. - // TODO: Can improve performance here a lot: - // 1. Do not import everything over and over again. Keep a cache. - // 2. Exempt the standard library; their directories always match the package name. - // 3. Can use build.FindOnly and then use parser.ParseDir with mode PackageClauseOnly - if pkgAlias == "" { - pkg, err := build.Import(fullPath, srcDir, 0) - if err != nil { - // We expect this to happen for apps using reverse routing (since we - // have not yet generated the routes). Don't log that. - if !strings.HasSuffix(fullPath, "/app/routes") { - utils.Logger.Info("Debug: Could not find import:", "path", fullPath) - } - continue - } - pkgAlias = pkg.Name - } - - imports[pkgAlias] = fullPath - } -} - -// If this Decl is a struct type definition, it is summarized and added to specs. -// Else, specs is returned unchanged. -func appendStruct(fileName string, specs []*model.TypeInfo, pkgImportPath string, pkg *ast.Package, decl ast.Decl, imports map[string]string, fset *token.FileSet) []*model.TypeInfo { - // Filter out non-Struct type declarations. - spec, found := getStructTypeDecl(decl, fset) - if !found { - return specs - } - - structType := spec.Type.(*ast.StructType) - - // At this point we know it's a type declaration for a struct. - // Fill in the rest of the info by diving into the fields. - // Add it provisionally to the Controller list -- it's later filtered using field info. - controllerSpec := &model.TypeInfo{ - StructName: spec.Name.Name, - ImportPath: pkgImportPath, - PackageName: pkg.Name, - } - - for _, field := range structType.Fields.List { - // If field.Names is set, it's not an embedded type. - if field.Names != nil { - continue - } - - // A direct "sub-type" has an ast.Field as either: - // Ident { "AppController" } - // SelectorExpr { "rev", "Controller" } - // Additionally, that can be wrapped by StarExprs. - fieldType := field.Type - pkgName, typeName := func() (string, string) { - // Drill through any StarExprs. - for { - if starExpr, ok := fieldType.(*ast.StarExpr); ok { - fieldType = starExpr.X - continue - } - break - } - - // If the embedded type is in the same package, it's an Ident. - if ident, ok := fieldType.(*ast.Ident); ok { - return "", ident.Name - } - - if selectorExpr, ok := fieldType.(*ast.SelectorExpr); ok { - if pkgIdent, ok := selectorExpr.X.(*ast.Ident); ok { - return pkgIdent.Name, selectorExpr.Sel.Name - } - } - return "", "" - }() - - // If a typename wasn't found, skip it. - if typeName == "" { - continue - } - - // Find the import path for this type. - // If it was referenced without a package name, use the current package import path. - // Else, look up the package's import path by name. - var importPath string - if pkgName == "" { - importPath = pkgImportPath - } else { - var ok bool - if importPath, ok = imports[pkgName]; !ok { - utils.Logger.Error("Error: Failed to find import path for ", "package", pkgName, "type", typeName) - continue - } - } - - controllerSpec.EmbeddedTypes = append(controllerSpec.EmbeddedTypes, &model.EmbeddedTypeName{ - ImportPath: importPath, - StructName: typeName, - }) - } - - return append(specs, controllerSpec) -} - -// If decl is a Method declaration, it is summarized and added to the array -// underneath its receiver type. -// e.g. "Login" => {MethodSpec, MethodSpec, ..} -func appendAction(fset *token.FileSet, mm methodMap, decl ast.Decl, pkgImportPath, pkgName string, imports map[string]string) { - // Func declaration? - funcDecl, ok := decl.(*ast.FuncDecl) - if !ok { - return - } - - // Have a receiver? - if funcDecl.Recv == nil { - return - } - - // Is it public? - if !funcDecl.Name.IsExported() { - return - } - - // Does it return a Result? - if funcDecl.Type.Results == nil || len(funcDecl.Type.Results.List) != 1 { - return - } - selExpr, ok := funcDecl.Type.Results.List[0].Type.(*ast.SelectorExpr) - if !ok { - return - } - if selExpr.Sel.Name != "Result" { - return - } - if pkgIdent, ok := selExpr.X.(*ast.Ident); !ok || imports[pkgIdent.Name] != model.RevelImportPath { - return - } - - method := &model.MethodSpec{ - Name: funcDecl.Name.Name, - } - - // Add a description of the arguments to the method. - for _, field := range funcDecl.Type.Params.List { - for _, name := range field.Names { - var importPath string - typeExpr := model.NewTypeExprFromAst(pkgName, field.Type) - if !typeExpr.Valid { - utils.Logger.Warn("Warn: Didn't understand argument '%s' of action %s. Ignoring.", name, getFuncName(funcDecl)) - return // We didn't understand one of the args. Ignore this action. - } - // Local object - if typeExpr.PkgName == pkgName { - importPath = pkgImportPath - } else if typeExpr.PkgName != "" { - var ok bool - if importPath, ok = imports[typeExpr.PkgName]; !ok { - utils.Logger.Fatalf("Failed to find import for arg of type: %s , %s", typeExpr.PkgName, typeExpr.TypeName("")) - } - } - method.Args = append(method.Args, &model.MethodArg{ - Name: name.Name, - TypeExpr: typeExpr, - ImportPath: importPath, - }) - } - } - - // Add a description of the calls to Render from the method. - // Inspect every node (e.g. always return true). - method.RenderCalls = []*model.MethodCall{} - ast.Inspect(funcDecl.Body, func(node ast.Node) bool { - // Is it a function call? - callExpr, ok := node.(*ast.CallExpr) - if !ok { - return true - } - - // Is it calling (*Controller).Render? - selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) - if !ok { - return true - } - - // The type of the receiver is not easily available, so just store every - // call to any method called Render. - if selExpr.Sel.Name != "Render" { - return true - } - - // Add this call's args to the renderArgs. - pos := fset.Position(callExpr.Lparen) - methodCall := &model.MethodCall{ - Line: pos.Line, - Names: []string{}, - } - for _, arg := range callExpr.Args { - argIdent, ok := arg.(*ast.Ident) - if !ok { - continue - } - methodCall.Names = append(methodCall.Names, argIdent.Name) - } - method.RenderCalls = append(method.RenderCalls, methodCall) - return true - }) - - var recvTypeName string - var recvType = funcDecl.Recv.List[0].Type - if recvStarType, ok := recvType.(*ast.StarExpr); ok { - recvTypeName = recvStarType.X.(*ast.Ident).Name - } else { - recvTypeName = recvType.(*ast.Ident).Name - } - - mm[recvTypeName] = append(mm[recvTypeName], method) -} - -// Scan app source code for calls to X.Y(), where X is of type *Validation. -// -// Recognize these scenarios: -// - "Y" = "Validation" and is a member of the receiver. -// (The common case for inline validation) -// - "X" is passed in to the func as a parameter. -// (For structs implementing Validated) -// -// The line number to which a validation call is attributed is that of the -// surrounding ExprStmt. This is so that it matches what runtime.Callers() -// reports. -// -// The end result is that we can set the default validation key for each call to -// be the same as the local variable. -func GetValidationKeys(fname string, fset *token.FileSet, funcDecl *ast.FuncDecl, imports map[string]string) map[int]string { - var ( - lineKeys = make(map[int]string) - - // Check the func parameters and the receiver's members for the *revel.Validation type. - validationParam = getValidationParameter(funcDecl, imports) - ) - - ast.Inspect(funcDecl.Body, func(node ast.Node) bool { - // e.g. c.Validation.Required(arg) or v.Required(arg) - callExpr, ok := node.(*ast.CallExpr) - if !ok { - return true - } - - // e.g. c.Validation.Required or v.Required - funcSelector, ok := callExpr.Fun.(*ast.SelectorExpr) - if !ok { - return true - } - - switch x := funcSelector.X.(type) { - case *ast.SelectorExpr: // e.g. c.Validation - if x.Sel.Name != "Validation" { - return true - } - - case *ast.Ident: // e.g. v - if validationParam == nil || x.Obj != validationParam { - return true - } - - default: - return true - } - - if len(callExpr.Args) == 0 { - return true - } - - // Given the validation expression, extract the key. - key := callExpr.Args[0] - switch expr := key.(type) { - case *ast.BinaryExpr: - // If the argument is a binary expression, take the first expression. - // (e.g. c.Validation.Required(myName != "")) - key = expr.X - case *ast.UnaryExpr: - // If the argument is a unary expression, drill in. - // (e.g. c.Validation.Required(!myBool) - key = expr.X - case *ast.BasicLit: - // If it's a literal, skip it. - return true - } - - if typeExpr := model.NewTypeExprFromAst("", key); typeExpr.Valid { - lineKeys[fset.Position(callExpr.End()).Line] = typeExpr.TypeName("") - } else { - utils.Logger.Error("Error: Failed to generate key for field validation. Make sure the field name is valid.", "file", fname, - "line", fset.Position(callExpr.End()).Line, "function", funcDecl.Name.String()) - } - return true - }) - - return lineKeys -} - -// Check to see if there is a *revel.Validation as an argument. -func getValidationParameter(funcDecl *ast.FuncDecl, imports map[string]string) *ast.Object { - for _, field := range funcDecl.Type.Params.List { - starExpr, ok := field.Type.(*ast.StarExpr) // e.g. *revel.Validation - if !ok { - continue - } - - selExpr, ok := starExpr.X.(*ast.SelectorExpr) // e.g. revel.Validation - if !ok { - continue - } - - xIdent, ok := selExpr.X.(*ast.Ident) // e.g. rev - if !ok { - continue - } - - if selExpr.Sel.Name == "Validation" && imports[xIdent.Name] == model.RevelImportPath { - return field.Names[0].Obj - } - } - return nil -} - // getStructTypeDecl checks if the given decl is a type declaration for a // struct. If so, the TypeSpec is returned. func getStructTypeDecl(decl ast.Decl, fset *token.FileSet) (spec *ast.TypeSpec, found bool) { @@ -599,24 +242,3 @@ func getStructTypeDecl(decl ast.Decl, fset *token.FileSet) (spec *ast.TypeSpec, return } - -func importPathFromPath(root string) string { - if vendorIdx := strings.Index(root, "/vendor/"); vendorIdx != -1 { - return filepath.ToSlash(root[vendorIdx+8:]) - } - for _, gopath := range filepath.SplitList(build.Default.GOPATH) { - srcPath := filepath.Join(gopath, "src") - if strings.HasPrefix(root, srcPath) { - return filepath.ToSlash(root[len(srcPath)+1:]) - } - } - - srcPath := filepath.Join(build.Default.GOROOT, "src", "pkg") - if strings.HasPrefix(root, srcPath) { - utils.Logger.Warn("Code path should be in GOPATH, but is in GOROOT:", "path", root) - return filepath.ToSlash(root[len(srcPath)+1:]) - } - - utils.Logger.Error("Unexpected! Code path is not in GOPATH:", "path", root) - return "" -} diff --git a/parser/validation.go b/parser/validation.go new file mode 100644 index 0000000..018fdda --- /dev/null +++ b/parser/validation.go @@ -0,0 +1,115 @@ +package parser + +import ( + "github.com/revel/cmd/model" + "github.com/revel/cmd/utils" + "go/ast" + "go/token" +) + +// Scan app source code for calls to X.Y(), where X is of type *Validation. +// +// Recognize these scenarios: +// - "Y" = "Validation" and is a member of the receiver. +// (The common case for inline validation) +// - "X" is passed in to the func as a parameter. +// (For structs implementing Validated) +// +// The line number to which a validation call is attributed is that of the +// surrounding ExprStmt. This is so that it matches what runtime.Callers() +// reports. +// +// The end result is that we can set the default validation key for each call to +// be the same as the local variable. +func GetValidationKeys(fname string, fset *token.FileSet, funcDecl *ast.FuncDecl, imports map[string]string) map[int]string { + var ( + lineKeys = make(map[int]string) + + // Check the func parameters and the receiver's members for the *revel.Validation type. + validationParam = getValidationParameter(funcDecl, imports) + ) + + ast.Inspect(funcDecl.Body, func(node ast.Node) bool { + // e.g. c.Validation.Required(arg) or v.Required(arg) + callExpr, ok := node.(*ast.CallExpr) + if !ok { + return true + } + + // e.g. c.Validation.Required or v.Required + funcSelector, ok := callExpr.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + + switch x := funcSelector.X.(type) { + case *ast.SelectorExpr: // e.g. c.Validation + if x.Sel.Name != "Validation" { + return true + } + + case *ast.Ident: // e.g. v + if validationParam == nil || x.Obj != validationParam { + return true + } + + default: + return true + } + + if len(callExpr.Args) == 0 { + return true + } + + // Given the validation expression, extract the key. + key := callExpr.Args[0] + switch expr := key.(type) { + case *ast.BinaryExpr: + // If the argument is a binary expression, take the first expression. + // (e.g. c.Validation.Required(myName != "")) + key = expr.X + case *ast.UnaryExpr: + // If the argument is a unary expression, drill in. + // (e.g. c.Validation.Required(!myBool) + key = expr.X + case *ast.BasicLit: + // If it's a literal, skip it. + return true + } + + if typeExpr := model.NewTypeExprFromAst("", key); typeExpr.Valid { + lineKeys[fset.Position(callExpr.End()).Line] = typeExpr.TypeName("") + } else { + utils.Logger.Error("Error: Failed to generate key for field validation. Make sure the field name is valid.", "file", fname, + "line", fset.Position(callExpr.End()).Line, "function", funcDecl.Name.String()) + } + return true + }) + + return lineKeys +} + +// Check to see if there is a *revel.Validation as an argument. +func getValidationParameter(funcDecl *ast.FuncDecl, imports map[string]string) *ast.Object { + for _, field := range funcDecl.Type.Params.List { + starExpr, ok := field.Type.(*ast.StarExpr) // e.g. *revel.Validation + if !ok { + continue + } + + selExpr, ok := starExpr.X.(*ast.SelectorExpr) // e.g. revel.Validation + if !ok { + continue + } + + xIdent, ok := selExpr.X.(*ast.Ident) // e.g. rev + if !ok { + continue + } + + if selExpr.Sel.Name == "Validation" && imports[xIdent.Name] == model.RevelImportPath { + return field.Names[0].Obj + } + } + return nil +} diff --git a/revel/build.go b/revel/build.go index c9a7387..e7e8126 100644 --- a/revel/build.go +++ b/revel/build.go @@ -17,7 +17,7 @@ import ( ) var cmdBuild = &Command{ - UsageLine: "build -i [import path] -t [target path] -r [run mode]", + UsageLine: "revel build [-r [run mode]] [import path] [target path] ", Short: "build a Revel application (e.g. for deployment)", Long: ` Build the Revel web application named by the given import path. @@ -25,7 +25,7 @@ This allows it to be deployed and run on a machine that lacks a Go installation. For example: - revel build -a github.com/revel/examples/chat -t /tmp/chat + revel build github.com/revel/examples/chat /tmp/chat `, } @@ -38,10 +38,12 @@ func init() { // The update config updates the configuration command so that it can run func updateBuildConfig(c *model.CommandConfig, args []string) bool { c.Index = model.BUILD + // If arguments were passed in then there must be two if len(args) < 2 { fmt.Fprintf(os.Stderr, "%s\n%s", cmdBuild.UsageLine, cmdBuild.Long) return false } + c.Build.ImportPath = args[0] c.Build.TargetPath = args[1] if len(args) > 2 { @@ -51,75 +53,101 @@ func updateBuildConfig(c *model.CommandConfig, args []string) bool { } // The main entry point to build application from command line -func buildApp(c *model.CommandConfig) { +func buildApp(c *model.CommandConfig) (err error) { appImportPath, destPath, mode := c.ImportPath, c.Build.TargetPath, DefaultRunMode if len(c.Build.Mode) > 0 { mode = c.Build.Mode } // Convert target to absolute path - destPath, _ = filepath.Abs(destPath) + c.Build.TargetPath, _ = filepath.Abs(destPath) + c.Build.Mode = mode - revel_paths := model.NewRevelPaths(mode, appImportPath, "", model.DoNothingRevelCallback) - - // First, verify that it is either already empty or looks like a previous - // build (to avoid clobbering anything) - if utils.Exists(destPath) && !utils.Empty(destPath) && !utils.Exists(filepath.Join(destPath, "run.sh")) { - utils.Logger.Errorf("Abort: %s exists and does not look like a build directory.", destPath) + revel_paths, err := model.NewRevelPaths(mode, appImportPath, "", model.NewWrappedRevelCallback(nil, c.PackageResolver)) + if err != nil { return } - if err := os.RemoveAll(destPath); err != nil && !os.IsNotExist(err) { - utils.Logger.Error("Remove all error", "error", err) - return - } + buildSafetyCheck(destPath) - if err := os.MkdirAll(destPath, 0777); err != nil { - utils.Logger.Error("makedir error", "error", err) - return + // Ensure the application can be built, this generates the main file + app, err := harness.Build(c, revel_paths) + if err != nil { + return err } - - app, reverr := harness.Build(c, revel_paths) - if reverr != nil { - utils.Logger.Error("Failed to build application", "error", reverr) - return - } - + // Copy files // Included are: // - run scripts // - binary // - revel // - app + packageFolders, err := buildCopyFiles(c, app, revel_paths) + if err != nil { + return + } + err = buildCopyModules(c, revel_paths, packageFolders) + if err != nil { + return + } + err = buildWriteScripts(c, app) + if err != nil { + return + } + return +} + +// Copy the files to the target +func buildCopyFiles(c *model.CommandConfig, app *harness.App, revel_paths *model.RevelContainer) (packageFolders []string, err error) { + appImportPath, destPath := c.ImportPath, c.Build.TargetPath + // Revel and the app are in a directory structure mirroring import path srcPath := filepath.Join(destPath, "src") destBinaryPath := filepath.Join(destPath, filepath.Base(app.BinaryPath)) tmpRevelPath := filepath.Join(srcPath, filepath.FromSlash(model.RevelImportPath)) - utils.MustCopyFile(destBinaryPath, app.BinaryPath) + if err = utils.CopyFile(destBinaryPath, app.BinaryPath); err != nil { + return + } utils.MustChmod(destBinaryPath, 0755) // Copy the templates from the revel - _ = utils.MustCopyDir(filepath.Join(tmpRevelPath, "conf"), filepath.Join(revel_paths.RevelPath, "conf"), nil) - _ = utils.MustCopyDir(filepath.Join(tmpRevelPath, "templates"), filepath.Join(revel_paths.RevelPath, "templates"), nil) + if err = utils.CopyDir(filepath.Join(tmpRevelPath, "conf"), filepath.Join(revel_paths.RevelPath, "conf"), nil); err != nil { + return + } + if err = utils.CopyDir(filepath.Join(tmpRevelPath, "templates"), filepath.Join(revel_paths.RevelPath, "templates"), nil); err != nil { + return + } // Get the folders to be packaged - packageFolders := strings.Split(revel_paths.Config.StringDefault("package.folders", "conf,public,app/views"), ",") - for i,p:=range packageFolders { + packageFolders = strings.Split(revel_paths.Config.StringDefault("package.folders", "conf,public,app/views"), ",") + for i, p := range packageFolders { // Clean spaces, reformat slash to filesystem - packageFolders[i]=filepath.FromSlash(strings.TrimSpace(p)) + packageFolders[i] = filepath.FromSlash(strings.TrimSpace(p)) } if c.Build.CopySource { - _ = utils.MustCopyDir(filepath.Join(srcPath, filepath.FromSlash(appImportPath)), revel_paths.BasePath, nil) + err = utils.CopyDir(filepath.Join(srcPath, filepath.FromSlash(appImportPath)), revel_paths.BasePath, nil) + if err != nil { + return + } } else { for _, folder := range packageFolders { - _ = utils.MustCopyDir( + err = utils.CopyDir( filepath.Join(srcPath, filepath.FromSlash(appImportPath), folder), filepath.Join(revel_paths.BasePath, folder), nil) + if err != nil { + return + } } } + return +} + +// Based on the section copy over the build modules +func buildCopyModules(c *model.CommandConfig, revel_paths *model.RevelContainer, packageFolders []string) (err error) { + destPath := filepath.Join(c.Build.TargetPath, "src") // Find all the modules used and copy them over. config := revel_paths.Config.Raw() modulePaths := make(map[string]string) // import path => filesystem path @@ -151,40 +179,76 @@ func buildApp(c *model.CommandConfig) { // Copy the the paths for each of the modules for importPath, fsPath := range modulePaths { - utils.Logger.Info("Copy files ", "to", filepath.Join(srcPath, importPath), "from", fsPath) + utils.Logger.Info("Copy files ", "to", filepath.Join(destPath, importPath), "from", fsPath) if c.Build.CopySource { - _ = utils.MustCopyDir(filepath.Join(srcPath, importPath), fsPath, nil) + err = utils.CopyDir(filepath.Join(destPath, importPath), fsPath, nil) + if err != nil { + return + } } else { for _, folder := range packageFolders { - _ = utils.MustCopyDir( - filepath.Join(srcPath, importPath, folder), + err = utils.CopyDir( + filepath.Join(destPath, importPath, folder), filepath.Join(fsPath, folder), nil) + if err != nil { + return + } } } - // } + return +} + +// Write the run scripts for the build +func buildWriteScripts(c *model.CommandConfig, app *harness.App) (err error) { tmplData := map[string]interface{}{ "BinName": filepath.Base(app.BinaryPath), - "ImportPath": appImportPath, - "Mode": mode, + "ImportPath": c.Build.ImportPath, + "Mode": c.Build.Mode, } - utils.MustGenerateTemplate( - filepath.Join(destPath, "run.sh"), + err = utils.GenerateTemplate( + filepath.Join(c.Build.TargetPath, "run.sh"), PACKAGE_RUN_SH, tmplData, ) - utils.MustChmod(filepath.Join(destPath, "run.sh"), 0755) - utils.MustGenerateTemplate( - filepath.Join(destPath, "run.bat"), + if err != nil { + return + } + utils.MustChmod(filepath.Join(c.Build.TargetPath, "run.sh"), 0755) + err = utils.GenerateTemplate( + filepath.Join(c.Build.TargetPath, "run.bat"), PACKAGE_RUN_BAT, tmplData, ) + if err != nil { + return + } - fmt.Println("Your application has been built in:", destPath) + fmt.Println("Your application has been built in:", c.Build.TargetPath) + return +} + +// Checks to see if the target folder exists and can be created +func buildSafetyCheck(destPath string) error { + + // First, verify that it is either already empty or looks like a previous + // build (to avoid clobbering anything) + if utils.Exists(destPath) && !utils.Empty(destPath) && !utils.Exists(filepath.Join(destPath, "run.sh")) { + return utils.NewBuildError("Abort: %s exists and does not look like a build directory.", "path", destPath) + } + + if err := os.RemoveAll(destPath); err != nil && !os.IsNotExist(err) { + return utils.NewBuildIfError(err, "Remove all error", "path", destPath) + } + + if err := os.MkdirAll(destPath, 0777); err != nil { + return utils.NewBuildIfError(err, "MkDir all error", "path", destPath) + } + return nil } const PACKAGE_RUN_SH = `#!/bin/sh diff --git a/revel/build_test.go b/revel/build_test.go new file mode 100644 index 0000000..b139066 --- /dev/null +++ b/revel/build_test.go @@ -0,0 +1,34 @@ +package main_test + +import ( + "github.com/revel/cmd/model" + "github.com/revel/cmd/revel" + "github.com/revel/cmd/utils" + "github.com/stretchr/testify/assert" + "os" + "path/filepath" + "testing" +) + +// test the commands +func TestBuild(t *testing.T) { + a := assert.New(t) + gopath := setup("revel-test-build", a) + + t.Run("Build", func(t *testing.T) { + a := assert.New(t) + c := newApp("build-test", model.NEW, nil, a) + main.Commands[model.NEW].RunWith(c) + c.Index = model.BUILD + c.Build.TargetPath = filepath.Join(gopath, "build-test", "target") + c.Build.ImportPath = c.ImportPath + a.Nil(main.Commands[model.BUILD].RunWith(c), "Failed to run build-test") + a.True(utils.Exists(filepath.Join(gopath, "build-test", "target"))) + }) + + if !t.Failed() { + if err := os.RemoveAll(gopath); err != nil { + a.Fail("Failed to remove test path") + } + } +} diff --git a/revel/clean.go b/revel/clean.go index a4aa9e1..6a210f3 100644 --- a/revel/clean.go +++ b/revel/clean.go @@ -14,14 +14,14 @@ import ( ) var cmdClean = &Command{ - UsageLine: "clean -i [import path]", + UsageLine: "clean [import path]", Short: "clean a Revel application's temp files", Long: ` Clean the Revel web application named by the given import path. For example: - revel clean -a github.com/revel/examples/chat + revel clean github.com/revel/examples/chat It removes the app/tmp and app/routes directory. @@ -46,7 +46,7 @@ func updateCleanConfig(c *model.CommandConfig, args []string) bool { } // Clean the source directory of generated files -func cleanApp(c *model.CommandConfig) { +func cleanApp(c *model.CommandConfig) (err error) { appPkg, err := build.Import(c.ImportPath, "", build.FindOnly) if err != nil { utils.Logger.Fatal("Abort: Failed to find import path:", "error", err) @@ -65,4 +65,5 @@ func cleanApp(c *model.CommandConfig) { return } } + return err } diff --git a/revel/clean_test.go b/revel/clean_test.go new file mode 100644 index 0000000..50dae6d --- /dev/null +++ b/revel/clean_test.go @@ -0,0 +1,37 @@ +package main_test + +import ( + "github.com/revel/cmd/model" + "github.com/revel/cmd/revel" + "github.com/revel/cmd/utils" + "github.com/stretchr/testify/assert" + "os" + "path/filepath" + "testing" +) + +// test the commands +func TestClean(t *testing.T) { + a := assert.New(t) + gopath := setup("revel-test-clean", a) + + + t.Run("Clean", func(t *testing.T) { + a := assert.New(t) + c := newApp("clean-test", model.NEW, nil, a) + main.Commands[model.NEW].RunWith(c) + c.Index = model.TEST + main.Commands[model.TEST].RunWith(c) + a.True(utils.Exists(filepath.Join(gopath, "src", "clean-test", "app", "tmp", "main.go")), + "Missing main from path "+filepath.Join(gopath, "src", "clean-test", "app", "tmp", "main.go")) + c.Clean.ImportPath = c.ImportPath + a.Nil(main.Commands[model.CLEAN].RunWith(c), "Failed to run clean-test") + a.False(utils.Exists(filepath.Join(gopath, "src", "clean-test", "app", "tmp", "main.go")), + "Did not remove main from path "+filepath.Join(gopath, "src", "clean-test", "app", "tmp", "main.go")) + }) + if !t.Failed() { + if err := os.RemoveAll(gopath); err != nil { + a.Fail("Failed to remove test path") + } + } +} diff --git a/revel/command_test.go b/revel/command_test.go new file mode 100644 index 0000000..cf1dcf8 --- /dev/null +++ b/revel/command_test.go @@ -0,0 +1,74 @@ +package main_test + +import ( + "github.com/revel/cmd/logger" + "github.com/revel/cmd/model" + "github.com/revel/cmd/utils" + "github.com/stretchr/testify/assert" + "go/build" + "os" + "path/filepath" +) + +// Test that the event handler can be attached and it dispatches the event received +func setup(suffix string, a *assert.Assertions) (string) { + temp := os.TempDir() + wd, _ := os.Getwd() + utils.InitLogger(wd, logger.LvlInfo) + gopath := filepath.Join(temp, "revel-test",suffix) + if utils.Exists(gopath) { + utils.Logger.Info("Removing test path", "path", gopath) + if err := os.RemoveAll(gopath); err != nil { + a.Fail("Failed to remove test path") + } + } + err := os.MkdirAll(gopath, os.ModePerm) + a.Nil(err, "Failed to create gopath "+gopath) + + // So this is the issue, on the mac when folders are created in a temp folder they are returned like + // /var/folders/nz/vv4_9tw56nv9k3tkvyszvwg80000gn/T/revel-test/revel-test-build + // But if you change into that directory and read the current folder it is + // /private/var/folders/nz/vv4_9tw56nv9k3tkvyszvwg80000gn/T/revel-test/revel-test-build + // So to make this work on darwin this code was added + os.Chdir(gopath) + newwd, _ := os.Getwd() + gopath = newwd + defaultBuild := build.Default + defaultBuild.GOPATH = gopath + build.Default = defaultBuild + utils.Logger.Info("Setup stats", "original wd", wd, "new wd", newwd, "gopath",gopath, "gopath exists", utils.DirExists(gopath), "wd exists", utils.DirExists(newwd)) + + return gopath +} + +// Create a new app for the name +func newApp(name string, command model.COMMAND, precall func(c *model.CommandConfig), a *assert.Assertions) *model.CommandConfig { + c := &model.CommandConfig{} + switch command { + case model.NEW: + c.New.ImportPath = name + case model.BUILD: + c.Build.ImportPath = name + case model.TEST: + c.Test.ImportPath = name + case model.PACKAGE: + c.Package.ImportPath = name + case model.VERSION: + c.Version.ImportPath = name + case model.CLEAN: + c.Clean.ImportPath = name + default: + a.Fail("Unknown command ", command) + } + + c.Index = command + if precall != nil { + precall(c) + } + if !c.UpdateImportPath() { + a.Fail("Unable to update import path") + } + c.InitGoPaths() + c.InitPackageResolver() + return c +} diff --git a/revel/new.go b/revel/new.go index 15116fd..4b91bd6 100644 --- a/revel/new.go +++ b/revel/new.go @@ -5,7 +5,6 @@ package main import ( - "bytes" "fmt" "go/build" "math/rand" @@ -16,6 +15,7 @@ import ( "github.com/revel/cmd/model" "github.com/revel/cmd/utils" + "net/url" ) var cmdNew = &Command{ @@ -51,83 +51,105 @@ func updateNewConfig(c *model.CommandConfig, args []string) bool { return false } c.New.ImportPath = args[0] - if len(args)>1 { - c.New.Skeleton = args[1] + if len(args) > 1 { + c.New.SkeletonPath = args[1] } return true } // Call to create a new application -func newApp(c *model.CommandConfig) { - // check for proper args by count - c.SkeletonPath = c.New.Skeleton - - // Check for an existing folder so we dont clober it - c.AppPath = filepath.Join(c.SrcRoot, filepath.FromSlash(c.ImportPath)) - _, err := build.Import(c.ImportPath, "", build.FindOnly) - if err==nil || !utils.Empty(c.AppPath) { - utils.Logger.Fatal("Abort: Import path already exists.","path", c.ImportPath) +func newApp(c *model.CommandConfig) (err error) { + // Check for an existing folder so we don't clobber it + _, err = build.Import(c.ImportPath, "", build.FindOnly) + if err == nil || !utils.Empty(c.AppPath) { + return utils.NewBuildError("Abort: Import path already exists.", "path", c.ImportPath) + } + if err := os.MkdirAll(c.AppPath, os.ModePerm); err != nil { + return utils.NewBuildError("Abort: Unable to create app path.", "path", c.AppPath) } if c.New.Vendored { - depPath, err := exec.LookPath("dep") - if err != nil { - // Do not halt build unless a new package needs to be imported - utils.Logger.Fatal("New: `dep` executable not found in PATH, but vendor folder requested." + - "You must install the dep tool before creating a vendored project. " + - "You can install the `dep` tool by doing a `go get -u github.com/golang/dep/cmd/dep`") - } - vendorPath := filepath.Join(c.ImportPath,"vendor") - if !utils.DirExists(vendorPath) { - err := os.MkdirAll(vendorPath,os.ModePerm) - utils.PanicOnError(err, "Failed to create " + vendorPath) - } - // In order for dep to run there needs to be a source file in the folder - tempPath := filepath.Join(c.ImportPath,"tmp") - if !utils.DirExists(tempPath) { - err := os.MkdirAll(tempPath,os.ModePerm) - utils.PanicOnError(err, "Failed to create " + vendorPath) - err = utils.MustGenerateTemplate(filepath.Join(tempPath,"main.go"), NEW_MAIN_FILE, nil) - utils.PanicOnError(err, "Failed to create main file " + vendorPath) + utils.Logger.Info("Creating a new vendor app") + vendorPath := filepath.Join(c.AppPath, "vendor") + if !utils.DirExists(vendorPath) { + + if err := os.MkdirAll(vendorPath, os.ModePerm); err != nil { + return utils.NewBuildError("Failed to create "+vendorPath, "error", err) + } } - packageFile := filepath.Join(c.ImportPath,"Gopkg.toml") + + // In order for dep to run there needs to be a source file in the folder + tempPath := filepath.Join(c.AppPath, "tmp") + utils.Logger.Info("Checking for temp folder for source code", "path", tempPath) + if !utils.DirExists(tempPath) { + if err := os.MkdirAll(tempPath, os.ModePerm); err != nil { + return utils.NewBuildIfError(err, "Failed to create "+vendorPath) + } + + if err = utils.GenerateTemplate(filepath.Join(tempPath, "main.go"), NEW_MAIN_FILE, nil); err != nil { + return utils.NewBuildIfError(err, "Failed to create main file "+vendorPath) + } + } + + // Create a package template file if it does not exist + packageFile := filepath.Join(c.AppPath, "Gopkg.toml") + utils.Logger.Info("Checking for Gopkg.toml", "path", packageFile) if !utils.Exists(packageFile) { - utils.MustGenerateTemplate(packageFile,VENDOR_GOPKG,nil) + utils.Logger.Info("Generating Gopkg.toml", "path", packageFile) + if err := utils.GenerateTemplate(packageFile, VENDOR_GOPKG, nil); err != nil { + return utils.NewBuildIfError(err, "Failed to generate template") + } } else { utils.Logger.Info("Package file exists in skeleto, skipping adding") } - getCmd := exec.Command(depPath, "ensure", "-v") - getCmd.Dir = c.ImportPath + getCmd := exec.Command("dep", "ensure", "-v") + utils.CmdInit(getCmd, c.AppPath) + utils.Logger.Info("Exec:", "args", getCmd.Args) + getOutput, err := getCmd.CombinedOutput() + if err != nil { + return utils.NewBuildIfError(err, string(getOutput)) + } + } + + // checking and setting application + + if err = setApplicationPath(c); err != nil { + return err + } + + // checking and setting skeleton + if err=setSkeletonPath(c);err!=nil { + return + } + + // copy files to new app directory + if err = copyNewAppFiles(c);err != nil { + return + } + + // Rerun the dep tool if vendored + if c.New.Vendored { + getCmd := exec.Command("dep", "ensure", "-v") + utils.CmdInit(getCmd, c.AppPath) utils.Logger.Info("Exec:", "args", getCmd.Args) - getCmd.Dir = c.ImportPath getOutput, err := getCmd.CombinedOutput() if err != nil { utils.Logger.Fatal(string(getOutput)) } } - - // checking and setting application - setApplicationPath(c) - - // checking and setting skeleton - setSkeletonPath(c) - - // copy files to new app directory - copyNewAppFiles(c) - - // goodbye world - fmt.Fprintln(os.Stdout, "Your application is ready:\n ", c.AppPath) + fmt.Fprintln(os.Stdout, "Your application has been created in:\n ", c.AppPath) // Check to see if it should be run right off if c.New.Run { runApp(c) } else { fmt.Fprintln(os.Stdout, "\nYou can run it with:\n revel run -a ", c.ImportPath) } + return } // Used to generate a new secret key @@ -143,7 +165,7 @@ func generateSecret() string { } // Sets the applicaiton path -func setApplicationPath(c *model.CommandConfig) { +func setApplicationPath(c *model.CommandConfig) (err error) { // revel/revel#1014 validate relative path, we cannot use built-in functions // since Go import path is valid relative path too. @@ -153,95 +175,134 @@ func setApplicationPath(c *model.CommandConfig) { c.ImportPath) } - // If we are running a vendored version of Revel we do not need to check for it. if !c.New.Vendored { - var err error _, err = build.Import(model.RevelImportPath, "", build.FindOnly) if err != nil { - // Go get the revel project - getCmd := exec.Command(c.GoCmd, "get", model.RevelImportPath) - utils.Logger.Info("Exec:" + c.GoCmd, "args", getCmd.Args) - getOutput, err := getCmd.CombinedOutput() + //// Go get the revel project + err = c.PackageResolver(model.RevelImportPath) if err != nil { - utils.Logger.Fatal("Failed to fetch revel " + model.RevelImportPath, "getOutput", string(getOutput)) + return utils.NewBuildIfError(err, "Failed to fetch revel "+model.RevelImportPath) } } } c.AppName = filepath.Base(c.AppPath) - c.BasePath = filepath.ToSlash(filepath.Dir(c.ImportPath)) - if c.BasePath == "." { - // we need to remove the a single '.' when - // the app is in the $GOROOT/src directory - c.BasePath = "" - } else { - // we need to append a '/' when the app is - // is a subdirectory such as $GOROOT/src/path/to/revelapp - c.BasePath += "/" - } + //if c.BasePath == "." { + // // we need to remove the a single '.' when + // // the app is in the $GOROOT/src directory + // c.BasePath = "" + //} else { + // // we need to append a '/' when the app is + // // is a subdirectory such as $GOROOT/src/path/to/revelapp + // c.BasePath += "/" + //} + + return nil } // Set the skeleton path -func setSkeletonPath(c *model.CommandConfig) { - var err error - if len(c.SkeletonPath) > 0 { // user specified - - _, err = build.Import(c.SkeletonPath, "", build.FindOnly) - if err != nil { - // Execute "go get " - getCmd := exec.Command(c.GoCmd, "get", "-d", c.SkeletonPath) - fmt.Println("Exec:", getCmd.Args) - getOutput, err := getCmd.CombinedOutput() - - // check getOutput for no buildible string - bpos := bytes.Index(getOutput, []byte("no buildable Go source files in")) - if err != nil && bpos == -1 { - utils.Logger.Fatalf("Abort: Could not find or 'go get' Skeleton source code: %s\n%s\n", getOutput, c.SkeletonPath) - } - } - // use the - c.SkeletonPath = filepath.Join(c.SrcRoot, c.SkeletonPath) - - } else { - // use the revel default - revelCmdPkg, err := build.Import(RevelCmdImportPath, "", build.FindOnly) - if err != nil { - if err != nil { - // Go get the revel project - getCmd := exec.Command(c.GoCmd, "get", RevelCmdImportPath + "/revel") - utils.Logger.Info("Exec:" + c.GoCmd, "args", getCmd.Args) - getOutput, err := getCmd.CombinedOutput() - if err != nil { - utils.Logger.Fatal("Failed to fetch revel cmd " + RevelCmdImportPath, "getOutput", string(getOutput)) - } - revelCmdPkg, err = build.Import(RevelCmdImportPath, "", build.FindOnly) - if err!= nil { - utils.Logger.Fatal("Failed to find source of revel cmd " + RevelCmdImportPath, "getOutput", string(getOutput), "error",err, "dir", revelCmdPkg.Dir) - } - } - } - - c.SkeletonPath = filepath.Join(revelCmdPkg.Dir, "revel", "skeleton") +func setSkeletonPath(c *model.CommandConfig) (err error) { + if len(c.New.SkeletonPath) == 0 { + c.New.SkeletonPath = RevelCmdImportPath + ":skeleton" } + + // First check to see the protocol of the string + if sp, err := url.Parse(c.New.SkeletonPath); err == nil { + utils.Logger.Info("Detected skeleton path", "path", sp) + + switch strings.ToLower(sp.Scheme) { + // TODO Add support for https, http, ftp, file + case "git": + if err := newLoadFromGit(c, sp); err != nil { + return err + } + case "": + if err := newLoadFromGo(c, sp); err != nil { + return err + } + default: + utils.Logger.Fatal("Unsupported") + + } + // TODO check to see if the path needs to be extracted + } else { + utils.Logger.Fatal("Invalid skeleton path format", "path", c.New.SkeletonPath) + } + return } -func copyNewAppFiles(c *model.CommandConfig) { - var err error - err = os.MkdirAll(c.AppPath, 0777) - utils.PanicOnError(err, "Failed to create directory "+c.AppPath) +// Load skeleton from git +func newLoadFromGit(c *model.CommandConfig, sp *url.URL) (err error) { + // This method indicates we need to fetch from a repository using git + // Execute "git clone get " + targetPath := filepath.Join(os.TempDir(), "revel", "skeleton") + os.RemoveAll(targetPath) + pathpart := strings.Split(sp.Path, ":") + getCmd := exec.Command("git", "clone", sp.Scheme+"://"+sp.Host+pathpart[0], targetPath) + utils.Logger.Info("Exec:", "args", getCmd.Args) + getOutput, err := getCmd.CombinedOutput() + if err != nil { + utils.Logger.Fatalf("Abort: could not clone the Skeleton source code: \n%s\n%s\n", getOutput, c.New.SkeletonPath) + } + outputPath := targetPath + if len(pathpart) > 1 { + outputPath = filepath.Join(targetPath, filepath.Join(strings.Split(pathpart[1], string('/'))...)) + } + outputPath, _ = filepath.Abs(outputPath) + if !strings.HasPrefix(outputPath, targetPath) { + utils.Logger.Fatal("Unusual target path outside root path", "target", outputPath, "root", targetPath) + } - _ = utils.MustCopyDir(c.AppPath, c.SkeletonPath, map[string]interface{}{ + c.New.SkeletonPath = outputPath + return +} + +// Load from GO +func newLoadFromGo(c *model.CommandConfig, sp *url.URL) (err error) { + // Find the source paths, download packages automatically + pathpart := strings.Split(sp.Path, ":") + _, skeletonImportPath , err := utils.FindSrcPaths(c.ImportPath,sp.Host+pathpart[0],c.PackageResolver) + if err!=nil { + return + } + + skeletonImportPath = filepath.Join(skeletonImportPath,sp.Host,pathpart[0]) + // Add in anything after the "Root" path + if len(pathpart) > 1 { + pathpart[0] = skeletonImportPath + newdir, _ := filepath.Abs(filepath.Join(pathpart...)) + if !strings.HasPrefix(newdir, skeletonImportPath) { + utils.Logger.Fatal("Unusual target path outside root path", "target", newdir, "root", skeletonImportPath) + } + skeletonImportPath = newdir + } + + c.New.SkeletonPath = skeletonImportPath + return +} + +func copyNewAppFiles(c *model.CommandConfig) (err error) { + err = os.MkdirAll(c.AppPath, 0777) + if err != nil { + return utils.NewBuildIfError(err, "MKDIR failed") + } + + err = utils.CopyDir(c.AppPath, c.New.SkeletonPath, map[string]interface{}{ // app.conf "AppName": c.AppName, - "BasePath": c.BasePath, + "BasePath": c.AppPath, "Secret": generateSecret(), }) + if err != nil { + fmt.Printf("err %v", err) + return utils.NewBuildIfError(err, "Copy Dir failed") + } // Dotfiles are skipped by mustCopyDir, so we have to explicitly copy the .gitignore. gitignore := ".gitignore" - utils.MustCopyFile(filepath.Join(c.AppPath, gitignore), filepath.Join(c.SkeletonPath, gitignore)) + return utils.CopyFile(filepath.Join(c.AppPath, gitignore), filepath.Join(c.New.SkeletonPath, gitignore)) } @@ -299,4 +360,4 @@ required = ["github.com/revel/cmd/revel"] NEW_MAIN_FILE = `package main ` -) \ No newline at end of file +) diff --git a/revel/new_test.go b/revel/new_test.go new file mode 100644 index 0000000..7eeec79 --- /dev/null +++ b/revel/new_test.go @@ -0,0 +1,107 @@ +package main_test + +import ( + "github.com/revel/cmd/model" + "github.com/revel/cmd/revel" + "github.com/revel/cmd/utils" + "github.com/stretchr/testify/assert" + "io/ioutil" + "os" + "path/filepath" + "testing" +) + +// test the commands +func TestNew(t *testing.T) { + a := assert.New(t) + gopath := setup("revel-test-new", a) + + t.Run("New", func(t *testing.T) { + a := assert.New(t) + c := newApp("new-test", model.NEW, nil, a) + a.Nil(main.Commands[model.NEW].RunWith(c), "New failed") + }) + t.Run("Path", func(t *testing.T) { + a := assert.New(t) + c := newApp("new/test/a", model.NEW, nil, a) + a.Nil(main.Commands[model.NEW].RunWith(c), "New path failed") + }) + t.Run("Path-Duplicate", func(t *testing.T) { + a := assert.New(t) + c := newApp("new/test/b", model.NEW, nil, a) + a.Nil(main.Commands[model.NEW].RunWith(c), "New path failed") + c = newApp("new/test/b", model.NEW, nil, a) + a.NotNil(main.Commands[model.NEW].RunWith(c), "Duplicate path Did Not failed") + }) + t.Run("Skeleton-Git", func(t *testing.T) { + a := assert.New(t) + c := newApp("new/test/c/1", model.NEW, nil, a) + c.New.SkeletonPath = "git://github.com/revel/cmd:skeleton2" + a.NotNil(main.Commands[model.NEW].RunWith(c), "Expected Failed to run with new") + // We need to pick a different path + c = newApp("new/test/c/2", model.NEW, nil, a) + c.New.SkeletonPath = "git://github.com/revel/cmd:skeleton" + a.Nil(main.Commands[model.NEW].RunWith(c), "Failed to run with new skeleton git") + }) + t.Run("Skeleton-Go", func(t *testing.T) { + a := assert.New(t) + c := newApp("new/test/d", model.NEW, nil, a) + c.New.SkeletonPath = "github.com/revel/cmd:skeleton" + a.Nil(main.Commands[model.NEW].RunWith(c), "Failed to run with new from go") + }) + if !t.Failed() { + if err := os.RemoveAll(gopath); err != nil { + a.Fail("Failed to remove test path") + } + } +} + +// test the commands +func TestNewVendor(t *testing.T) { + a := assert.New(t) + gopath := setup("revel-test-new-vendor", a) + precall := func(c *model.CommandConfig) { + c.New.Vendored = true + } + t.Run("New", func(t *testing.T) { + a := assert.New(t) + c := newApp("onlyone/v/a", model.NEW, precall, a) + c.New.Vendored = true + a.Nil(main.Commands[model.NEW].RunWith(c), "New failed") + }) + t.Run("Test", func(t *testing.T) { + a := assert.New(t) + c := newApp("onlyone/v/a", model.TEST, nil, a) + a.Nil(main.Commands[model.TEST].RunWith(c), "Test failed") + }) + t.Run("Build", func(t *testing.T) { + a := assert.New(t) + c := newApp("onlyone/v/a", model.BUILD, nil, a) + c.Index = model.BUILD + c.Build.TargetPath = filepath.Join(gopath, "src/onlyone/v/a", "target") + a.Nil(main.Commands[model.BUILD].RunWith(c), " Build failed") + a.True(utils.DirExists(c.Build.TargetPath), "Target folder not made", c.Build.TargetPath) + }) + t.Run("Package", func(t *testing.T) { + a := assert.New(t) + c := newApp("onlyone/v/a", model.PACKAGE, nil, a) + c.Package.TargetPath = filepath.Join(gopath, "src/onlyone/v/a", "target.tar.gz") + a.Nil(main.Commands[model.PACKAGE].RunWith(c), "Package Failed") + a.True(utils.Exists(c.Package.TargetPath), "Target package not made", c.Package.TargetPath) + }) + t.Run("TestVendorDir", func(t *testing.T) { + // Check to see that no additional packages were downloaded outside the vendor folder + files, err := ioutil.ReadDir(gopath) + a.Nil(err, "Failed to read gopath folder") + // bin/ onlyone/ pkg/ src/ + a.Equal(3, len(files), "Expected single file in "+gopath) + files, err = ioutil.ReadDir(filepath.Join(gopath, "src")) + a.Nil(err, "Failed to read src folder") + a.Equal(1, len(files), "Expected single file in source folder", filepath.Join(gopath, "src")) + }) + if !t.Failed() { + if err := os.RemoveAll(gopath); err != nil { + a.Fail("Failed to remove test path") + } + } +} diff --git a/revel/package.go b/revel/package.go index c8b1314..963d62c 100644 --- a/revel/package.go +++ b/revel/package.go @@ -15,7 +15,7 @@ import ( ) var cmdPackage = &Command{ - UsageLine: "package -i [import path] -r [run mode]", + UsageLine: "package [-r [run mode]] [application] ", Short: "package a Revel application (e.g. for deployment)", Long: ` Package the Revel web application named by the given import path. @@ -28,7 +28,7 @@ Run mode defaults to "dev". For example: - revel package -i github.com/revel/examples/chat + revel package github.com/revel/examples/chat `, } @@ -40,19 +40,16 @@ func init() { // Called when unable to parse the command line automatically and assumes an old launch func updatePackageConfig(c *model.CommandConfig, args []string) bool { c.Index = model.PACKAGE - if len(args) == 0 { - fmt.Fprintf(os.Stderr, cmdPackage.Long) - return false - } c.Package.ImportPath = args[0] - if len(args)>1 { + if len(args) > 1 { c.Package.Mode = args[1] } return true } -func packageApp(c *model.CommandConfig) { +// Called to package the app +func packageApp(c *model.CommandConfig) (err error) { // Determine the run mode. mode := DefaultRunMode @@ -61,13 +58,22 @@ func packageApp(c *model.CommandConfig) { } appImportPath := c.ImportPath - revel_paths := model.NewRevelPaths(mode, appImportPath, "", model.DoNothingRevelCallback) + revel_paths, err := model.NewRevelPaths(mode, appImportPath, "", model.NewWrappedRevelCallback(nil, c.PackageResolver)) + if err != nil { + return + } // Remove the archive if it already exists. - destFile := filepath.Base(revel_paths.BasePath) + ".tar.gz" + destFile := filepath.Join(c.AppPath, filepath.Base(revel_paths.BasePath)+".tar.gz") + if c.Package.TargetPath != "" { + if filepath.IsAbs(c.Package.TargetPath) { + destFile = c.Package.TargetPath + } else { + destFile = filepath.Join(c.AppPath, c.Package.TargetPath) + } + } if err := os.Remove(destFile); err != nil && !os.IsNotExist(err) { - utils.Logger.Error("Unable to remove target file","error",err,"file",destFile) - os.Exit(1) + return utils.NewBuildError("Unable to remove target file", "error", err, "file", destFile) } // Collect stuff in a temp directory. @@ -83,7 +89,12 @@ func packageApp(c *model.CommandConfig) { buildApp(c) // Create the zip file. - archiveName := utils.MustTarGzDir(destFile, tmpDir) + + archiveName, err := utils.TarGzDir(destFile, tmpDir) + if err != nil { + return + } fmt.Println("Your archive is ready:", archiveName) + return } diff --git a/revel/package_test.go b/revel/package_test.go new file mode 100644 index 0000000..02e7e35 --- /dev/null +++ b/revel/package_test.go @@ -0,0 +1,30 @@ +package main_test + +import ( + "github.com/revel/cmd/model" + "github.com/revel/cmd/revel" + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +// test the commands +func TestPackage(t *testing.T) { + a := assert.New(t) + gopath := setup("revel-test-package", a) + + t.Run("Package", func(t *testing.T) { + a := assert.New(t) + c := newApp("package-test", model.NEW, nil, a) + main.Commands[model.NEW].RunWith(c) + c.Index = model.PACKAGE + c.Package.ImportPath = c.ImportPath + a.Nil(main.Commands[model.PACKAGE].RunWith(c), "Failed to run package-test") + }) + + if !t.Failed() { + if err := os.RemoveAll(gopath); err != nil { + a.Fail("Failed to remove test path") + } + } +} diff --git a/revel/revel.go b/revel/revel.go index e0cc6a7..d7bff11 100644 --- a/revel/revel.go +++ b/revel/revel.go @@ -8,23 +8,18 @@ package main import ( "flag" "fmt" - "io" "math/rand" "os" "runtime" "strings" - "text/template" "time" "github.com/jessevdk/go-flags" "github.com/agtorre/gocolorize" + "github.com/revel/cmd/logger" "github.com/revel/cmd/model" "github.com/revel/cmd/utils" - "github.com/revel/cmd/logger" - "os/exec" - "path/filepath" - "go/build" ) const ( @@ -37,8 +32,8 @@ const ( // Command structure cribbed from the genius organization of the "go" command. type Command struct { - UpdateConfig func(c *model.CommandConfig, args []string) bool - RunWith func(c *model.CommandConfig) + UpdateConfig func(c *model.CommandConfig, args []string) bool + RunWith func(c *model.CommandConfig) error UsageLine, Short, Long string } @@ -53,7 +48,7 @@ func (cmd *Command) Name() string { } // The commands -var commands = []*Command{ +var Commands = []*Command{ nil, // Safety net, prevent missing index from running cmdNew, cmdRun, @@ -63,43 +58,61 @@ var commands = []*Command{ cmdTest, cmdVersion, } + func main() { if runtime.GOOS == "windows" { gocolorize.SetPlain(true) } c := &model.CommandConfig{} - wd,_ := os.Getwd() + wd, _ := os.Getwd() - utils.InitLogger(wd,logger.LvlError) + utils.InitLogger(wd, logger.LvlError) - parser := flags.NewParser(c, flags.HelpFlag | flags.PassDoubleDash) - if ini:=flag.String("ini","none","");*ini!="none" { - if err:=flags.NewIniParser(parser).ParseFile(*ini);err!=nil { - utils.Logger.Error("Unable to load ini", "error",err) + parser := flags.NewParser(c, flags.HelpFlag|flags.PassDoubleDash) + if err := ParseArgs(c, parser, os.Args[1:]); err != nil { + fmt.Fprint(os.Stderr, err.Error()) + parser.WriteHelp(os.Stdout) + os.Exit(1) + } + + // Switch based on the verbose flag + if len(c.Verbose)>1 { + utils.InitLogger(wd, logger.LvlDebug) + } else if len(c.Verbose)>0 { + utils.InitLogger(wd, logger.LvlInfo) + } else { + utils.InitLogger(wd, logger.LvlWarn) + } + + if !c.UpdateImportPath() { + utils.Logger.Fatal("Unable to determine application path") + } + + command := Commands[c.Index] + println("Revel executing:", command.Short) + + // Setting go paths + c.InitGoPaths() + + // Setup package resolver + c.InitPackageResolver() + + if err := command.RunWith(c); err != nil { + utils.Logger.Error("Unable to execute","error",err) + os.Exit(1) + } +} + +// Parse the arguments passed into the model.CommandConfig +func ParseArgs(c *model.CommandConfig, parser *flags.Parser, args []string) (err error) { + var extraArgs []string + if ini := flag.String("ini", "none", ""); *ini != "none" { + if err = flags.NewIniParser(parser).ParseFile(*ini); err != nil { + return } } else { - if _, err := parser.Parse(); err != nil { - utils.Logger.Info("Command line options failed", "error", err.Error()) - - // Decode nature of error - if perr,ok:=err.(*flags.Error); ok { - if perr.Type == flags.ErrRequired { - // Try the old way - if !main_parse_old(c) { - println("Command line error:", err.Error()) - parser.WriteHelp(os.Stdout) - os.Exit(1) - } - } else { - println("Command line error:", err.Error()) - parser.WriteHelp(os.Stdout) - os.Exit(1) - } - } else { - println("Command line error:", err.Error()) - parser.WriteHelp(os.Stdout) - os.Exit(1) - } + if extraArgs, err = parser.ParseArgs(args); err != nil { + return } else { switch parser.Active.Name { case "new": @@ -120,182 +133,18 @@ func main() { } } - // Switch based on the verbose flag - if c.Verbose { - utils.InitLogger(wd, logger.LvlDebug) - } else { - utils.InitLogger(wd, logger.LvlWarn) - } - - if c.Index==0 { - utils.Logger.Fatal("Unknown command line arguements") - } - if !c.UpdateImportPath() { - utils.Logger.Fatal("Unable to determine application path") - } - println("Revel executing:", commands[c.Index].Short) - // checking and setting go paths - initGoPaths(c) - - - commands[c.Index].RunWith(c) - - -} - -// Try to populate the CommandConfig using the old techniques -func main_parse_old(c *model.CommandConfig) bool { - // Take the old command format and try to parse them - flag.Usage = func() { usage(1) } - flag.Parse() - args := flag.Args() - - if len(args) < 1 || args[0] == "help" { - if len(args) == 1 { - usage(0) - } - - if len(args) > 1 { - for _, cmd := range commands { - if cmd!=nil && cmd.Name() == args[1] { - tmpl(os.Stdout, helpTemplate, cmd) - return false - } - } - } - usage(2) - } - - for _, cmd := range commands { - if cmd!=nil && cmd.Name() == args[0] { - println("Running", cmd.Name()) - return cmd.UpdateConfig(c, args[1:]) + if c.Index == 0 { + err = fmt.Errorf("Unknown command %v", extraArgs) + } else if len(extraArgs) > 0 { + utils.Logger.Info("Found additional arguements, setting them") + if !Commands[c.Index].UpdateConfig(c, extraArgs) { + err = fmt.Errorf("Invalid command line arguements %v", extraArgs) } } - return false -} - -func main_old() { - if runtime.GOOS == "windows" { - gocolorize.SetPlain(true) - } - fmt.Fprintf(os.Stdout, gocolorize.NewColor("blue").Paint(header)) - flag.Usage = func() { usage(1) } - flag.Parse() - args := flag.Args() - - if len(args) < 1 || args[0] == "help" { - if len(args) == 1 { - usage(0) - } - if len(args) > 1 { - for _, cmd := range commands { - if cmd.Name() == args[1] { - tmpl(os.Stdout, helpTemplate, cmd) - return - } - } - } - usage(2) - } - - // Commands use panic to abort execution when something goes wrong. - // Panics are logged at the point of error. Ignore those. - defer func() { - if err := recover(); err != nil { - if _, ok := err.(utils.LoggedError); !ok { - // This panic was not expected / logged. - panic(err) - } - os.Exit(1) - } - }() - - //for _, cmd := range commands { - // if cmd.Name() == args[0] { - // cmd.UpdateConfig(args[1:]) - // return - // } - //} - - utils.Logger.Fatalf("unknown command %q\nRun 'revel help' for usage.\n", args[0]) -} - -const header = `~ -~ revel! http://revel.github.io -~ -` - -const usageTemplate = `usage: revel command [arguments] - -The commands are: -{{range .}} - {{.Name | printf "%-11s"}} {{.Short}}{{end}} - -Use "revel help [command]" for more information. -` - -var helpTemplate = `usage: revel {{.UsageLine}} -{{.Long}} -` - -func usage(exitCode int) { - tmpl(os.Stderr, usageTemplate, commands) - os.Exit(exitCode) -} - -func tmpl(w io.Writer, text string, data interface{}) { - t := template.New("top") - template.Must(t.Parse(text)) - if err := t.Execute(w, data); err != nil { - panic(err) - } + return } func init() { rand.Seed(time.Now().UnixNano()) } - -// lookup and set Go related variables -func initGoPaths(c *model.CommandConfig) { - // lookup go path - c.GoPath = build.Default.GOPATH - if c.GoPath == "" { - utils.Logger.Fatal("Abort: GOPATH environment variable is not set. " + - "Please refer to http://golang.org/doc/code.html to configure your Go environment.") - } - - // check for go executable - var err error - c.GoCmd, err = exec.LookPath("go") - if err != nil { - utils.Logger.Fatal("Go executable not found in PATH.") - } - - // revel/revel#1004 choose go path relative to current working directory - workingDir, _ := os.Getwd() - goPathList := filepath.SplitList(c.GoPath) - for _, path := range goPathList { - if strings.HasPrefix(strings.ToLower(workingDir), strings.ToLower(path)) { - c.SrcRoot = path - break - } - - path, _ = filepath.EvalSymlinks(path) - if len(path) > 0 && strings.HasPrefix(strings.ToLower(workingDir), strings.ToLower(path)) { - c.SrcRoot = path - break - } - } - - if len(c.SrcRoot) == 0 { - if c.Index != model.VERSION { - utils.Logger.Fatal("Abort: could not create a Revel application outside of GOPATH.") - } - return - } - - // set go src path - c.SrcRoot = filepath.Join(c.SrcRoot, "src") -} \ No newline at end of file diff --git a/revel/run.go b/revel/run.go index 04f22ac..166518b 100644 --- a/revel/run.go +++ b/revel/run.go @@ -15,23 +15,23 @@ import ( ) var cmdRun = &Command{ - UsageLine: "run [import path] [run mode] [port]", + UsageLine: "run [-m [run mode] -p [port]] [import path] ", Short: "run a Revel application", Long: ` Run the Revel web application named by the given import path. For example, to run the chat room sample application: - revel run github.com/revel/examples/chat dev + revel run -m dev github.com/revel/examples/chat The run mode is used to select which set of app.conf configuration should apply and may be used to determine logic in the application itself. Run mode defaults to "dev". -You can set a port as an optional third parameter. For example: +You can set a port as well. For example: - revel run github.com/revel/examples/chat prod 8080`, + revel run -m prod -p 8080 github.com/revel/examples/chat `, } // RunArgs holds revel run parameters @@ -47,14 +47,23 @@ func init() { } func updateRunConfig(c *model.CommandConfig, args []string) bool { - + convertPort := func(value string) int { + if value != "" { + port, err := strconv.Atoi(value) + if err != nil { + utils.Logger.Fatalf("Failed to parse port as integer: %s", c.Run.Port) + } + return port + } + return 0 + } switch len(args) { case 3: // Possible combinations // revel run [import-path] [run-mode] [port] c.Run.ImportPath = args[0] c.Run.Mode = args[1] - c.Run.Port = args[2] + c.Run.Port = convertPort(args[2]) case 2: // Possible combinations // 1. revel run [import-path] [run-mode] @@ -68,7 +77,7 @@ func updateRunConfig(c *model.CommandConfig, args []string) bool { if _, err := strconv.Atoi(args[1]); err == nil { // 2nd arg is the port number - c.Run.Port = args[1] + c.Run.Port = convertPort(args[1]) } else { // 2nd arg is the run mode c.Run.Mode = args[1] @@ -76,7 +85,7 @@ func updateRunConfig(c *model.CommandConfig, args []string) bool { } else { // 1st arg is the run mode c.Run.Mode = args[0] - c.Run.Port = args[1] + c.Run.Port = convertPort(args[1]) } case 1: // Possible combinations @@ -93,7 +102,7 @@ func updateRunConfig(c *model.CommandConfig, args []string) bool { c.Run.ImportPath = args[0] } else if _, err := strconv.Atoi(args[0]); err == nil { // 1st arg is the port number - c.Run.Port = args[0] + c.Run.Port = convertPort(args[0]) } else { // 1st arg is the run mode c.Run.Mode = args[0] @@ -105,18 +114,20 @@ func updateRunConfig(c *model.CommandConfig, args []string) bool { return true } -func runApp(c *model.CommandConfig) { +// Called to run the app +func runApp(c *model.CommandConfig) (err error) { if c.Run.Mode == "" { c.Run.Mode = "dev" } - revel_path := model.NewRevelPaths(c.Run.Mode, c.ImportPath, "", model.DoNothingRevelCallback) - if c.Run.Port != "" { - port, err := strconv.Atoi(c.Run.Port) - if err != nil { - utils.Logger.Fatalf("Failed to parse port as integer: %s", c.Run.Port) - } - revel_path.HTTPPort = port + revel_path, err := model.NewRevelPaths(c.Run.Mode, c.ImportPath, "", model.NewWrappedRevelCallback(nil, c.PackageResolver)) + if err != nil { + return utils.NewBuildIfError(err, "Revel paths") + } + if c.Run.Port > -1 { + revel_path.HTTPPort = c.Run.Port + } else { + c.Run.Port = revel_path.HTTPPort } utils.Logger.Infof("Running %s (%s) in %s mode\n", revel_path.AppName, revel_path.ImportPath, revel_path.RunMode) @@ -145,4 +156,5 @@ func runApp(c *model.CommandConfig) { runMode = revel_path.RunMode } app.Cmd(runMode).Run() + return } diff --git a/revel/run_test.go b/revel/run_test.go new file mode 100644 index 0000000..7a39b77 --- /dev/null +++ b/revel/run_test.go @@ -0,0 +1,21 @@ +package main_test + +import ( + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +// test the commands +func TestRun(t *testing.T) { + a := assert.New(t) + gopath := setup("revel-test-run", a) + + // TODO Testing run + + if !t.Failed() { + if err := os.RemoveAll(gopath); err != nil { + a.Fail("Failed to remove test path") + } + } +} diff --git a/revel/test.go b/revel/test.go index 9f1d920..d1ec415 100644 --- a/revel/test.go +++ b/revel/test.go @@ -22,7 +22,7 @@ import ( ) var cmdTest = &Command{ - UsageLine: "test [import path] [run mode] [suite.method]", + UsageLine: "test [ ]", Short: "run all tests from the command-line", Long: ` Run all tests for the Revel app named by the given import path. @@ -71,38 +71,38 @@ func updateTestConfig(c *model.CommandConfig, args []string) bool { } // Called to test the application -func testApp(c *model.CommandConfig) { - var err error - +func testApp(c *model.CommandConfig) (err error) { mode := DefaultRunMode if c.Test.Mode != "" { mode = c.Test.Mode } // Find and parse app.conf - revel_path := model.NewRevelPaths(mode, c.ImportPath, "", model.DoNothingRevelCallback) + revel_path, err := model.NewRevelPaths(mode, c.ImportPath, "", model.NewWrappedRevelCallback(nil, c.PackageResolver)) + if err != nil { + return + } - // Ensure that the testrunner is loaded in this mode. - // todo checkTestRunner() + // todo Ensure that the testrunner is loaded in this mode. // Create a directory to hold the test result files. resultPath := filepath.Join(revel_path.BasePath, "test-results") if err = os.RemoveAll(resultPath); err != nil { - utils.Logger.Errorf("Failed to remove test result directory %s: %s", resultPath, err) + return utils.NewBuildError("Failed to remove test result directory ", "path", resultPath, "error", err) } if err = os.Mkdir(resultPath, 0777); err != nil { - utils.Logger.Errorf("Failed to create test result directory %s: %s", resultPath, err) + return utils.NewBuildError("Failed to create test result directory ", "path", resultPath, "error", err) } // Direct all the output into a file in the test-results directory. file, err := os.OpenFile(filepath.Join(resultPath, "app.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) if err != nil { - utils.Logger.Errorf("Failed to create test result log file: %s", err) + return utils.NewBuildError("Failed to create test result log file: ", "error", err) } app, reverr := harness.Build(c, revel_path) if reverr != nil { - utils.Logger.Errorf("Error building: %s", reverr) + return utils.NewBuildIfError(reverr, "Error building: ") } runMode := fmt.Sprintf(`{"mode":"%s","testModeFlag":true, "specialUseFlag":%v}`, app.Paths.RunMode, c.Verbose) if c.HistoricMode { @@ -115,7 +115,7 @@ func testApp(c *model.CommandConfig) { // Start the app... if err := cmd.Start(c); err != nil { - utils.Logger.Errorf("%s", err) + return utils.NewBuildError("Unable to start server", "error", err) } defer cmd.Kill() @@ -164,6 +164,8 @@ func testApp(c *model.CommandConfig) { writeResultFile(resultPath, "result.failed", "failed") utils.Logger.Errorf("Some tests failed. See file://%s for results.", resultPath) } + + return } // Outputs the results to a file @@ -287,7 +289,7 @@ func runTestSuites(paths *model.RevelContainer, baseURL, resultPath string, test err = json.NewDecoder(resp.Body).Decode(&testResult) if err == nil && !testResult.Passed { suiteResult.Passed = false - utils.Logger.Error("Test Failed","suite", suite.Name, "test", test.Name) + utils.Logger.Error("Test Failed", "suite", suite.Name, "test", test.Name) fmt.Printf(" %s.%s : FAILED\n", suite.Name, test.Name) } else { fmt.Printf(" %s.%s : PASSED\n", suite.Name, test.Name) @@ -306,7 +308,9 @@ func runTestSuites(paths *model.RevelContainer, baseURL, resultPath string, test // Create the result HTML file. suiteResultFilename := filepath.Join(resultPath, fmt.Sprintf("%s.%s.html", suite.Name, strings.ToLower(suiteResultStr))) - utils.MustRenderTemplate(suiteResultFilename, resultFilePath, suiteResult) + if err := utils.RenderTemplate(suiteResultFilename, resultFilePath, suiteResult); err != nil { + utils.Logger.Error("Failed to render template", "error", err) + } } return &failedResults, overallSuccess diff --git a/revel/test_test.go b/revel/test_test.go new file mode 100644 index 0000000..adf5f5b --- /dev/null +++ b/revel/test_test.go @@ -0,0 +1,31 @@ +package main_test + +import ( + "github.com/revel/cmd/model" + "github.com/revel/cmd/revel" + "github.com/stretchr/testify/assert" + "os" + "testing" +) + + +// test the commands +func TestRevelTest(t *testing.T) { + a := assert.New(t) + gopath := setup("revel-test-test", a) + + t.Run("Test", func(t *testing.T) { + a := assert.New(t) + c := newApp("test-test", model.NEW, nil, a) + a.Nil(main.Commands[model.NEW].RunWith(c), "Failed to run test-test") + c.Index = model.TEST + c.Test.ImportPath = c.ImportPath + a.Nil(main.Commands[model.TEST].RunWith(c), "Failed to run test-test") + }) + + if !t.Failed() { + if err := os.RemoveAll(gopath); err != nil { + a.Fail("Failed to remove test path") + } + } +} diff --git a/revel/version.go b/revel/version.go index 7c7d1b1..a9021e8 100644 --- a/revel/version.go +++ b/revel/version.go @@ -12,67 +12,73 @@ import ( "fmt" "runtime" - "github.com/revel/cmd/model" - "go/build" - "go/token" - "go/parser" - "go/ast" - "io/ioutil" - "path/filepath" - "github.com/revel/cmd/utils" "github.com/revel/cmd" + "github.com/revel/cmd/model" + "github.com/revel/cmd/utils" + "go/ast" + "go/build" + "go/parser" + "go/token" + "io/ioutil" + "os" + "os/exec" + "path/filepath" ) var cmdVersion = &Command{ - UsageLine: "version", + UsageLine: "revel version", Short: "displays the Revel Framework and Go version", Long: ` Displays the Revel Framework and Go version. For example: - revel version + revel version [] `, } func init() { cmdVersion.RunWith = versionApp + cmdVersion.UpdateConfig = updateVersionConfig +} + +// Update the version +func updateVersionConfig(c *model.CommandConfig, args []string) bool { + if len(args) > 0 { + c.Version.ImportPath = args[0] + } + return true } // Displays the version of go and Revel -func versionApp(c *model.CommandConfig) { +func versionApp(c *model.CommandConfig) (err error) { - var ( - revelPkg *build.Package - err error - ) - if len(c.ImportPath)>0 { - appPkg, err := build.Import(c.ImportPath, "", build.FindOnly) - if err != nil { - utils.Logger.Fatal("Failed to import " + c.ImportPath + " with error:", "error", err) - } - revelPkg, err = build.Import(model.RevelImportPath, appPkg.Dir, build.FindOnly) - } else { - revelPkg, err = build.Import(model.RevelImportPath, "" , build.FindOnly) + var revelPath, appPath string + + + appPath, revelPath, err = utils.FindSrcPaths(c.ImportPath, model.RevelImportPath, c.PackageResolver) + if err != nil { + return utils.NewBuildError("Failed to import "+c.ImportPath+" with error:", "error", err) } + revelPath = revelPath + model.RevelImportPath - fmt.Println("\nRevel Framework") + fmt.Println("\nRevel Framework",revelPath, appPath ) if err != nil { utils.Logger.Info("Failed to find Revel in GOPATH with error:", "error", err, "gopath", build.Default.GOPATH) fmt.Println("Information not available (not on GOPATH)") } else { - utils.Logger.Info("Fullpath to revel", revelPkg.Dir) + utils.Logger.Info("Fullpath to revel", "dir", revelPath) fset := token.NewFileSet() // positions are relative to fset - version, err := ioutil.ReadFile(filepath.Join(revelPkg.Dir, "version.go")) + version, err := ioutil.ReadFile(filepath.Join(revelPath, "version.go")) if err != nil { - utils.Logger.Errorf("Failed to find Revel version:", "error", err) + utils.Logger.Error("Failed to find Revel version:", "error", err, "path", revelPath) } // Parse src but stop after processing the imports. f, err := parser.ParseFile(fset, "", version, parser.ParseComments) if err != nil { - utils.Logger.Errorf("Failed to parse Revel version error:", "error", err) + return utils.NewBuildError("Failed to parse Revel version error:", "error", err) } // Print the imports from the file's AST. @@ -96,5 +102,19 @@ func versionApp(c *model.CommandConfig) { fmt.Println("Build Date", cmd.BuildDate) fmt.Println("Minimum Go Version", cmd.MinimumGoVersion) - fmt.Printf("\n %s %s/%s\n\n", runtime.Version(), runtime.GOOS, runtime.GOARCH) + fmt.Printf("Compiled By %s %s/%s\n\n", runtime.Version(), runtime.GOOS, runtime.GOARCH) + // Extract the goversion detected + if len(c.GoCmd) > 0 { + cmd := exec.Command(c.GoCmd, "version") + cmd.Stdout = os.Stdout + if e := cmd.Start(); e != nil { + fmt.Println("Go command error ", e) + } else { + cmd.Wait() + } + } else { + fmt.Println("Go command not found ") + } + + return } diff --git a/revel/version_test.go b/revel/version_test.go new file mode 100644 index 0000000..4762721 --- /dev/null +++ b/revel/version_test.go @@ -0,0 +1,41 @@ +package main_test + +import ( + "github.com/revel/cmd/model" + "github.com/revel/cmd/revel" + "github.com/stretchr/testify/assert" + "os" + "path/filepath" + "testing" +) + +// test the commands +func TestVersion(t *testing.T) { + a := assert.New(t) + gopath := setup("revel-test-version", a) + + t.Run("Version", func(t *testing.T) { + a := assert.New(t) + c := newApp("version-test", model.NEW, nil, a) + a.Nil(main.Commands[model.NEW].RunWith(c), "Check new") + c.Build.ImportPath = c.ImportPath + c.Build.TargetPath = filepath.Join(gopath, "build-test", "target") + a.Nil(main.Commands[model.BUILD].RunWith(c), "Failed to run build") + c.Index = model.VERSION + c.Version.ImportPath = c.ImportPath + a.Nil(main.Commands[model.VERSION].RunWith(c), "Failed to run version-test") + }) + t.Run("Version-Nobuild", func(t *testing.T) { + a := assert.New(t) + c := newApp("version-test2", model.NEW, nil, a) + a.Nil(main.Commands[model.NEW].RunWith(c), "Check new") + c.Index = model.VERSION + c.Version.ImportPath = c.ImportPath + a.Nil(main.Commands[model.VERSION].RunWith(c), "Failed to run version-test") + }) + if !t.Failed() { + if err := os.RemoveAll(gopath); err != nil { + a.Fail("Failed to remove test path") + } + } +} diff --git a/revel/skeleton/.gitignore b/skeleton/.gitignore similarity index 100% rename from revel/skeleton/.gitignore rename to skeleton/.gitignore diff --git a/revel/skeleton/README.md b/skeleton/README.md similarity index 100% rename from revel/skeleton/README.md rename to skeleton/README.md diff --git a/revel/skeleton/app/controllers/app.go b/skeleton/app/controllers/app.go similarity index 100% rename from revel/skeleton/app/controllers/app.go rename to skeleton/app/controllers/app.go diff --git a/revel/skeleton/app/init.go b/skeleton/app/init.go similarity index 100% rename from revel/skeleton/app/init.go rename to skeleton/app/init.go diff --git a/revel/skeleton/app/views/App/Index.html b/skeleton/app/views/App/Index.html similarity index 100% rename from revel/skeleton/app/views/App/Index.html rename to skeleton/app/views/App/Index.html diff --git a/revel/skeleton/app/views/debug.html b/skeleton/app/views/debug.html similarity index 100% rename from revel/skeleton/app/views/debug.html rename to skeleton/app/views/debug.html diff --git a/revel/skeleton/app/views/errors/404.html b/skeleton/app/views/errors/404.html similarity index 100% rename from revel/skeleton/app/views/errors/404.html rename to skeleton/app/views/errors/404.html diff --git a/revel/skeleton/app/views/errors/500.html b/skeleton/app/views/errors/500.html similarity index 100% rename from revel/skeleton/app/views/errors/500.html rename to skeleton/app/views/errors/500.html diff --git a/revel/skeleton/app/views/flash.html b/skeleton/app/views/flash.html similarity index 100% rename from revel/skeleton/app/views/flash.html rename to skeleton/app/views/flash.html diff --git a/revel/skeleton/app/views/footer.html b/skeleton/app/views/footer.html similarity index 100% rename from revel/skeleton/app/views/footer.html rename to skeleton/app/views/footer.html diff --git a/revel/skeleton/app/views/header.html b/skeleton/app/views/header.html similarity index 100% rename from revel/skeleton/app/views/header.html rename to skeleton/app/views/header.html diff --git a/revel/skeleton/conf/app.conf.template b/skeleton/conf/app.conf.template similarity index 100% rename from revel/skeleton/conf/app.conf.template rename to skeleton/conf/app.conf.template diff --git a/revel/skeleton/conf/routes b/skeleton/conf/routes similarity index 100% rename from revel/skeleton/conf/routes rename to skeleton/conf/routes diff --git a/revel/skeleton/messages/sample.en b/skeleton/messages/sample.en similarity index 100% rename from revel/skeleton/messages/sample.en rename to skeleton/messages/sample.en diff --git a/revel/skeleton/public/css/bootstrap-3.3.6.min.css b/skeleton/public/css/bootstrap-3.3.6.min.css similarity index 100% rename from revel/skeleton/public/css/bootstrap-3.3.6.min.css rename to skeleton/public/css/bootstrap-3.3.6.min.css diff --git a/revel/skeleton/public/fonts/glyphicons-halflings-regular.ttf b/skeleton/public/fonts/glyphicons-halflings-regular.ttf similarity index 100% rename from revel/skeleton/public/fonts/glyphicons-halflings-regular.ttf rename to skeleton/public/fonts/glyphicons-halflings-regular.ttf diff --git a/revel/skeleton/public/fonts/glyphicons-halflings-regular.woff b/skeleton/public/fonts/glyphicons-halflings-regular.woff similarity index 100% rename from revel/skeleton/public/fonts/glyphicons-halflings-regular.woff rename to skeleton/public/fonts/glyphicons-halflings-regular.woff diff --git a/revel/skeleton/public/fonts/glyphicons-halflings-regular.woff2 b/skeleton/public/fonts/glyphicons-halflings-regular.woff2 similarity index 100% rename from revel/skeleton/public/fonts/glyphicons-halflings-regular.woff2 rename to skeleton/public/fonts/glyphicons-halflings-regular.woff2 diff --git a/revel/skeleton/public/img/favicon.png b/skeleton/public/img/favicon.png similarity index 100% rename from revel/skeleton/public/img/favicon.png rename to skeleton/public/img/favicon.png diff --git a/revel/skeleton/public/js/bootstrap-3.3.6.min.js b/skeleton/public/js/bootstrap-3.3.6.min.js similarity index 100% rename from revel/skeleton/public/js/bootstrap-3.3.6.min.js rename to skeleton/public/js/bootstrap-3.3.6.min.js diff --git a/revel/skeleton/public/js/jquery-2.2.4.min.js b/skeleton/public/js/jquery-2.2.4.min.js similarity index 100% rename from revel/skeleton/public/js/jquery-2.2.4.min.js rename to skeleton/public/js/jquery-2.2.4.min.js diff --git a/revel/skeleton/tests/apptest.go b/skeleton/tests/apptest.go similarity index 100% rename from revel/skeleton/tests/apptest.go rename to skeleton/tests/apptest.go diff --git a/tests/testrunner.go b/tests/testrunner.go index d3f24d8..6117873 100644 --- a/tests/testrunner.go +++ b/tests/testrunner.go @@ -13,7 +13,6 @@ import ( "github.com/revel/cmd/utils" ) - // TestSuiteDesc is used for storing information about a single test suite. // This structure is required by revel test cmd. type TestSuiteDesc struct { @@ -147,7 +146,6 @@ func errorSummary(err *utils.Error) (message string) { return } - //sortbySuiteName sorts the testsuites by name. type sortBySuiteName []interface{} diff --git a/utils/build_error.go b/utils/build_error.go new file mode 100644 index 0000000..6412bbb --- /dev/null +++ b/utils/build_error.go @@ -0,0 +1,45 @@ +package utils + +import ( + "fmt" + "github.com/revel/cmd/logger" +) + +type ( + BuildError struct { + Stack interface{} + Message string + Args []interface{} + } +) + +// Returns a new builed error +func NewBuildError(message string, args ...interface{}) (b *BuildError) { + Logger.Info(message, args...) + b = &BuildError{} + b.Message = message + b.Args = args + b.Stack = logger.NewCallStack() + Logger.Info("Stack", "stack", b.Stack) + return b +} + +// Returns a new BuildError if err is not nil +func NewBuildIfError(err error, message string, args ...interface{}) (b error) { + if err != nil { + if berr, ok := err.(*BuildError); ok { + // This is already a build error so just append the args + berr.Args = append(berr.Args, args...) + return berr + } else { + args = append(args, "error", err.Error()) + b = NewBuildError(message, args...) + } + } + return +} + +// BuildError implements Error() string +func (b *BuildError) Error() string { + return fmt.Sprint(b.Message, b.Args) +} diff --git a/utils/command.go b/utils/command.go new file mode 100644 index 0000000..29b76ce --- /dev/null +++ b/utils/command.go @@ -0,0 +1,27 @@ +package utils + +import ( + "go/build" + "os" + "os/exec" + "strings" +) + +// Initialize the command based on the GO environment +func CmdInit(c *exec.Cmd, basePath string) { + c.Dir = basePath + // Go 1.8 fails if we do not include the GOROOT + c.Env = []string{"GOPATH=" + build.Default.GOPATH, "PATH=" + GetEnv("PATH"), "GOROOT="+ GetEnv("GOROOT")} + +} + +// Returns an environment variable +func GetEnv(name string) string { + for _, v := range os.Environ() { + split := strings.Split(v, "=") + if split[0] == name { + return strings.Join(split[1:], "") + } + } + return "" +} diff --git a/utils/error.go b/utils/error.go index 08b3180..121590b 100644 --- a/utils/error.go +++ b/utils/error.go @@ -7,15 +7,22 @@ import ( ) // The error is a wrapper for the -type Error struct { - SourceType string // The type of source that failed to build. - Title, Path, Description string // Description of the error, as presented to the user. - Line, Column int // Where the error was encountered. - SourceLines []string // The entire source file, split into lines. - Stack string // The raw stack trace string from debug.Stack(). - MetaError string // Error that occurred producing the error page. - Link string // A configurable link to wrap the error source in -} +type ( + Error struct { + SourceType string // The type of source that failed to build. + Title, Path, Description string // Description of the error, as presented to the user. + Line, Column int // Where the error was encountered. + SourceLines []string // The entire source file, split into lines. + Stack string // The raw stack trace string from debug.Stack(). + MetaError string // Error that occurred producing the error page. + Link string // A configurable link to wrap the error source in + } + SourceLine struct { + Source string + Line int + IsError bool + } +) // Creates a link based on the configuration setting "errors.link" func (e *Error) SetLink(errorLink string) { @@ -50,6 +57,7 @@ func (e *Error) Error() string { } return fmt.Sprintf("%s%s", header, e.Description) } + // ContextSource method returns a snippet of the source around // where the error occurred. func (e *Error) ContextSource() []SourceLine { @@ -72,10 +80,3 @@ func (e *Error) ContextSource() []SourceLine { } return lines } - -// SourceLine structure to hold the per-source-line details. -type SourceLine struct { - Source string - Line int - IsError bool -} diff --git a/utils/file.go b/utils/file.go index c134f43..e51ddef 100644 --- a/utils/file.go +++ b/utils/file.go @@ -1,21 +1,21 @@ package utils - -// DirExists returns true if the given path exists and is a directory. import ( - "os" "archive/tar" - "strings" - "io" - "path/filepath" - "fmt" - "html/template" - "compress/gzip" - "go/build" - "io/ioutil" "bytes" + "compress/gzip" + "errors" + "fmt" + "go/build" + "html/template" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" ) +// DirExists returns true if the given path exists and is a directory. func DirExists(filename string) bool { fileInfo, err := os.Stat(filename) return err == nil && fileInfo.IsDir() @@ -39,49 +39,59 @@ func ReadLines(filename string) ([]string, error) { return strings.Split(string(dataBytes), "\n"), nil } -func MustCopyFile(destFilename, srcFilename string) { +// Copy file returns error +func CopyFile(destFilename, srcFilename string) (err error) { + destFile, err := os.Create(destFilename) - PanicOnError(err, "Failed to create file "+destFilename) + if err != nil { + return NewBuildIfError(err, "Failed to create file", "file", destFilename) + } srcFile, err := os.Open(srcFilename) - PanicOnError(err, "Failed to open file "+srcFilename) + if err != nil { + return NewBuildIfError(err, "Failed to open file", "file", srcFilename) + } _, err = io.Copy(destFile, srcFile) - PanicOnError(err, - fmt.Sprintf("Failed to copy data from %s to %s", srcFile.Name(), destFile.Name())) + if err != nil { + return NewBuildIfError(err, "Failed to copy data", "fromfile", srcFilename, "tofile", destFilename) + } err = destFile.Close() - PanicOnError(err, "Failed to close file "+destFile.Name()) + if err != nil { + return NewBuildIfError(err, "Failed to close file", "file", destFilename) + } err = srcFile.Close() - PanicOnError(err, "Failed to close file "+srcFile.Name()) -} + if err != nil { + return NewBuildIfError(err, "Failed to close file", "file", srcFilename) + } + return +} // GenerateTemplate renders the given template to produce source code, which it writes // to the given file. -func MustGenerateTemplate(filename, templateSource string, args map[string]interface{}) (err error) { +func GenerateTemplate(filename, templateSource string, args map[string]interface{}) (err error) { tmpl := template.Must(template.New("").Parse(templateSource)) var b bytes.Buffer if err = tmpl.Execute(&b, args); err != nil { - Logger.Fatal("ExecuteTemplate: Execute failed", "error", err) - return + return NewBuildIfError(err, "ExecuteTemplate: Execute failed") } sourceCode := b.String() filePath := filepath.Dir(filename) if !DirExists(filePath) { err = os.MkdirAll(filePath, 0777) if err != nil && !os.IsExist(err) { - Logger.Fatal("Failed to make directory","dir", filePath, "error", err) + return NewBuildIfError(err, "Failed to make directory", "dir", filePath) } } - // Create the file file, err := os.Create(filename) if err != nil { - Logger.Fatal("Failed to create file","error", err) + Logger.Fatal("Failed to create file", "error", err) return } defer func() { @@ -96,27 +106,41 @@ func MustGenerateTemplate(filename, templateSource string, args map[string]inter } // Given the target path and source path and data. A template -func MustRenderTemplate(destPath, srcPath string, data interface{}) { +func RenderTemplate(destPath, srcPath string, data interface{}) (err error) { tmpl, err := template.ParseFiles(srcPath) - PanicOnError(err, "Failed to parse template "+srcPath) + if err != nil { + return NewBuildIfError(err, "Failed to parse template "+srcPath) + } f, err := os.Create(destPath) - PanicOnError(err, "Failed to create "+destPath) + if err != nil { + return NewBuildIfError(err, "Failed to create ", "path", destPath) + } err = tmpl.Execute(f, data) - PanicOnError(err, "Failed to render template "+srcPath) + if err != nil { + return NewBuildIfError(err, "Failed to Render template "+srcPath) + } err = f.Close() - PanicOnError(err, "Failed to close "+f.Name()) + if err != nil { + return NewBuildIfError(err, "Failed to close file stream "+destPath) + } + return } // Given the target path and source path and data. A template -func MustRenderTemplateToStream(output io.Writer, srcPath []string, data interface{}) { +func RenderTemplateToStream(output io.Writer, srcPath []string, data interface{}) (err error) { tmpl, err := template.ParseFiles(srcPath...) - PanicOnError(err, "Failed to parse template "+srcPath[0]) + if err != nil { + return NewBuildIfError(err, "Failed to parse template "+srcPath[0]) + } err = tmpl.Execute(output, data) - PanicOnError(err, "Failed to render template "+srcPath[0]) + if err != nil { + return NewBuildIfError(err, "Failed to render template "+srcPath[0]) + } + return } func MustChmod(filename string, mode os.FileMode) { @@ -127,8 +151,7 @@ func MustChmod(filename string, mode os.FileMode) { // Called if panic func PanicOnError(err error, msg string) { if revErr, ok := err.(*Error); (ok && revErr != nil) || (!ok && err != nil) { - Logger.Fatalf("Abort: %s: %s %s\n", msg, revErr, err) - //panic(NewLoggedError(err)) + Logger.Panicf("Abort: %s: %s %s\n", msg, revErr, err) } } @@ -136,7 +159,7 @@ func PanicOnError(err error, msg string) { // ".template" are treated as a Go template and rendered using the given data. // Additionally, the trailing ".template" is stripped from the file name. // Also, dot files and dot directories are skipped. -func MustCopyDir(destDir, srcDir string, data map[string]interface{}) error { +func CopyDir(destDir, srcDir string, data map[string]interface{}) error { return fsWalk(srcDir, srcDir, func(srcPath string, info os.FileInfo, err error) error { // Get the relative path from the source base, and the corresponding path in // the dest directory. @@ -155,26 +178,29 @@ func MustCopyDir(destDir, srcDir string, data map[string]interface{}) error { if info.IsDir() { err := os.MkdirAll(filepath.Join(destDir, relSrcPath), 0777) if !os.IsExist(err) { - PanicOnError(err, "Failed to create directory") + return NewBuildIfError(err, "Failed to create directory", "path", destDir+"/"+relSrcPath) } return nil } // If this file ends in ".template", render it as a template. if strings.HasSuffix(relSrcPath, ".template") { - MustRenderTemplate(destPath[:len(destPath)-len(".template")], srcPath, data) - return nil + + return RenderTemplate(destPath[:len(destPath)-len(".template")], srcPath, data) } // Else, just copy it over. - MustCopyFile(destPath, srcPath) - return nil + + return CopyFile(destPath, srcPath) }) } +// Shortcut to fsWalk func Walk(root string, walkFn filepath.WalkFunc) error { - return fsWalk(root,root,walkFn) + return fsWalk(root, root, walkFn) } + +// Walk the tree using the function func fsWalk(fname string, linkName string, walkFn filepath.WalkFunc) error { fsWalkFunc := func(path string, info os.FileInfo, err error) error { if err != nil { @@ -214,9 +240,13 @@ func fsWalk(fname string, linkName string, walkFn filepath.WalkFunc) error { return err } -func MustTarGzDir(destFilename, srcDir string) string { +// Tar gz the folder +func TarGzDir(destFilename, srcDir string) (name string, err error) { zipFile, err := os.Create(destFilename) - PanicOnError(err, "Failed to create archive") + if err != nil { + return "", NewBuildIfError(err, "Failed to create archive", "file", destFilename) + } + defer func() { _ = zipFile.Close() }() @@ -231,13 +261,16 @@ func MustTarGzDir(destFilename, srcDir string) string { _ = tarWriter.Close() }() - _ = fsWalk(srcDir, srcDir, func(srcPath string, info os.FileInfo, err error) error { + err = fsWalk(srcDir, srcDir, func(srcPath string, info os.FileInfo, err error) error { if info.IsDir() { return nil } srcFile, err := os.Open(srcPath) - PanicOnError(err, "Failed to read source file") + if err != nil { + return NewBuildIfError(err, "Failed to read file", "file", srcPath) + } + defer func() { _ = srcFile.Close() }() @@ -248,17 +281,22 @@ func MustTarGzDir(destFilename, srcDir string) string { Mode: int64(info.Mode()), ModTime: info.ModTime(), }) - PanicOnError(err, "Failed to write tar entry header") + if err != nil { + return NewBuildIfError(err, "Failed to write tar entry header", "file", srcPath) + } _, err = io.Copy(tarWriter, srcFile) - PanicOnError(err, "Failed to copy") + if err != nil { + return NewBuildIfError(err, "Failed to copy file", "file", srcPath) + } return nil }) - return zipFile.Name() + return zipFile.Name(), err } +// Return true if the file exists func Exists(filename string) bool { _, err := os.Stat(filename) return err == nil @@ -278,8 +316,50 @@ func Empty(dirname string) bool { return len(results) == 0 } -func ImportPathFromCurrentDir() string { - pwd, _ := os.Getwd() - importPath, _ := filepath.Rel(filepath.Join(build.Default.GOPATH, "src"), pwd) - return filepath.ToSlash(importPath) +// Find the full source dir for the import path, uses the build.Default.GOPATH to search for the directory +func FindSrcPaths(appImportPath, revelImportPath string, packageResolver func(pkgName string) error) (appSourcePath, revelSourcePath string, err error) { + var ( + gopaths = filepath.SplitList(build.Default.GOPATH) + goroot = build.Default.GOROOT + ) + + if len(gopaths) == 0 { + err = errors.New("GOPATH environment variable is not set. " + + "Please refer to http://golang.org/doc/code.html to configure your Go environment.") + return + } + + if ContainsString(gopaths, goroot) { + err = fmt.Errorf("GOPATH (%s) must not include your GOROOT (%s). "+ + "Please refer to http://golang.org/doc/code.html to configure your Go environment. ", + build.Default.GOPATH, goroot) + return + + } + + appPkgDir := "" + appPkgSrcDir := "" + if len(appImportPath)>0 { + Logger.Info("Seeking app package","app",appImportPath) + appPkg, err := build.Import(appImportPath, "", build.FindOnly) + if err != nil { + err = fmt.Errorf("Failed to import " + appImportPath + " with error %s", err.Error()) + return "","",err + } + appPkgDir,appPkgSrcDir =appPkg.Dir, appPkg.SrcRoot + } + Logger.Info("Seeking remote package","using",appImportPath, "remote",revelImportPath) + revelPkg, err := build.Default.Import(revelImportPath, appPkgDir, build.FindOnly) + if err != nil { + Logger.Info("Resolved called Seeking remote package","using",appImportPath, "remote",revelImportPath) + packageResolver(revelImportPath) + revelPkg, err = build.Import(revelImportPath, appPkgDir, build.FindOnly) + if err != nil { + err = fmt.Errorf("Failed to find Revel with error: %s", err.Error()) + return + } + } + + revelSourcePath, appSourcePath = revelPkg.Dir[:len(revelPkg.Dir)-len(revelImportPath)], appPkgSrcDir + return } diff --git a/utils/log.go b/utils/log.go index f3c4033..29a61fb 100644 --- a/utils/log.go +++ b/utils/log.go @@ -1,9 +1,9 @@ package utils import ( + "fmt" "github.com/revel/cmd/logger" "github.com/revel/config" - "fmt" "os" "strings" ) @@ -12,16 +12,21 @@ var Logger = logger.New() func InitLogger(basePath string, logLevel logger.LogLevel) { newContext := config.NewContext() - if logLevel= logger.LvlInfo { + newContext.SetOption("log.info.output", "stdout") + } else { + newContext.SetOption("log.inf.output", "off") + } + + newContext.SetOption("log.warn.output", "stderr") + newContext.SetOption("log.error.output", "stderr") + newContext.SetOption("log.crit.output", "stderr") Logger.SetHandler(logger.InitializeFromConfig(basePath, newContext)) } diff --git a/version.go b/version.go index 3931cfc..3450feb 100644 --- a/version.go +++ b/version.go @@ -6,10 +6,10 @@ package cmd const ( // Version current Revel Command version - Version = "0.20.0-dev" + Version = "0.20.1" // BuildDate latest commit/release date - BuildDate = "2018-02-06" + BuildDate = "2018-09-30" // MinimumGoVersion minimum required Go version for Revel MinimumGoVersion = ">= go1.8" diff --git a/watcher/watcher.go b/watcher/watcher.go index 1e34101..c2d19fa 100644 --- a/watcher/watcher.go +++ b/watcher/watcher.go @@ -52,9 +52,9 @@ type Watcher struct { // Creates a new watched based on the container func NewWatcher(paths *model.RevelContainer, eagerRefresh bool) *Watcher { return &Watcher{ - forceRefresh: true, - lastError: -1, - paths: paths, + forceRefresh: true, + lastError: -1, + paths: paths, refreshTimerMS: time.Duration(paths.Config.IntDefault("watch.rebuild.delay", 10)), eagerRefresh: eagerRefresh || paths.DevMode &&