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