dev-tools/mage/settings.go (735 lines of code) (raw):

// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. package mage import ( "errors" "fmt" "go/build" "log" "os" "path" "path/filepath" "regexp" "strconv" "strings" "sync" "time" "golang.org/x/text/cases" "golang.org/x/text/language" "gopkg.in/yaml.v3" "github.com/magefile/mage/sh" "github.com/elastic/elastic-agent/dev-tools/mage/gotool" v1 "github.com/elastic/elastic-agent/pkg/api/v1" ) const ( fpmVersion = "1.13.1" // Docker images. See https://github.com/elastic/golang-crossbuild. beatsFPMImage = "docker.elastic.co/beats-dev/fpm" // BeatsCrossBuildImage is the image used for crossbuilding Beats. BeatsCrossBuildImage = "docker.elastic.co/beats-dev/golang-crossbuild" elasticAgentImportPath = "github.com/elastic/elastic-agent" elasticAgentModulePath = "github.com/elastic/elastic-agent" defaultName = "elastic-agent" // Env vars // agent package version agentPackageVersionEnvVar = "AGENT_PACKAGE_VERSION" //ManifestUrlEnvVar is the name fo the environment variable containing the Manifest URL to be used for packaging agent ManifestUrlEnvVar = "MANIFEST_URL" // AgentCommitHashEnvVar allows to override agent commit hash string during packaging AgentCommitHashEnvVar = "AGENT_COMMIT_HASH_OVERRIDE" // Mapped functions agentPackageVersionMappedFunc = "agent_package_version" agentManifestGeneratorMappedFunc = "manifest" snapshotSuffix = "snapshot_suffix" ) // Common settings with defaults derived from files, CWD, and environment. var ( GOOS = build.Default.GOOS GOARCH = build.Default.GOARCH GOARM = EnvOr("GOARM", "") Platform = MakePlatformAttributes(GOOS, GOARCH, GOARM) BinaryExt = "" XPackDir = "../x-pack" RaceDetector = false TestCoverage = false PLATFORMS = EnvOr("PLATFORMS", "") PACKAGES = EnvOr("PACKAGES", "") CI = EnvOr("CI", "") // CrossBuildMountModcache mounts $GOPATH/pkg/mod into // the crossbuild images at /go/pkg/mod, read-only, when set to true. CrossBuildMountModcache = true BeatName = EnvOr("BEAT_NAME", defaultName) BeatServiceName = EnvOr("BEAT_SERVICE_NAME", BeatName) BeatIndexPrefix = EnvOr("BEAT_INDEX_PREFIX", BeatName) BeatDescription = EnvOr("BEAT_DESCRIPTION", "") BeatVendor = EnvOr("BEAT_VENDOR", "Elastic") BeatLicense = EnvOr("BEAT_LICENSE", "Elastic License 2.0") BeatURL = EnvOr("BEAT_URL", "https://www.elastic.co/beats/"+BeatName) BeatUser = EnvOr("BEAT_USER", "root") BeatProjectType ProjectType Snapshot bool DevBuild bool ExternalBuild bool FIPSBuild bool versionQualified bool versionQualifier string // Env var to set the agent package version agentPackageVersion string // PackagingFromManifest This value is set to tru when we have defined a ManifestURL variable PackagingFromManifest bool // ManifestURL Location of the manifest file to package ManifestURL string FuncMap = map[string]interface{}{ "beat_doc_branch": BeatDocBranch, "beat_version": BeatQualifiedVersion, "commit": CommitHash, "commit_short": CommitHashShort, "date": BuildDate, "elastic_beats_dir": ElasticBeatsDir, "go_version": GoVersion, "repo": GetProjectRepoInfo, "title": func(s string) string { return cases.Title(language.English, cases.NoLower).String(s) }, "tolower": strings.ToLower, "contains": strings.Contains, "substring": Substring, agentPackageVersionMappedFunc: AgentPackageVersion, agentManifestGeneratorMappedFunc: PackageManifest, snapshotSuffix: SnapshotSuffix, } ) func init() { initGlobals() } func initGlobals() { if GOOS == "windows" { BinaryExt = ".exe" } var err error RaceDetector, err = strconv.ParseBool(EnvOr("RACE_DETECTOR", "false")) if err != nil { panic(fmt.Errorf("failed to parse RACE_DETECTOR env value: %w", err)) } TestCoverage, err = strconv.ParseBool(EnvOr("TEST_COVERAGE", "false")) if err != nil { panic(fmt.Errorf("failed to parse TEST_COVERAGE env value: %w", err)) } Snapshot, err = strconv.ParseBool(EnvOr("SNAPSHOT", "false")) if err != nil { panic(fmt.Errorf("failed to parse SNAPSHOT env value: %w", err)) } DevBuild, err = strconv.ParseBool(EnvOr("DEV", "false")) if err != nil { panic(fmt.Errorf("failed to parse DEV env value: %w", err)) } ExternalBuild, err = strconv.ParseBool(EnvOr("EXTERNAL", "false")) if err != nil { panic(fmt.Errorf("failed to parse EXTERNAL env value: %w", err)) } FIPSBuild, err = strconv.ParseBool(EnvOr("FIPS", "false")) if err != nil { panic(fmt.Errorf("failed to parse FIPS env value: %w", err)) } versionQualifier, versionQualified = os.LookupEnv("VERSION_QUALIFIER") agentPackageVersion = EnvOr(agentPackageVersionEnvVar, "") ManifestURL = EnvOr(ManifestUrlEnvVar, "") PackagingFromManifest = ManifestURL != "" } // ProjectType specifies the type of project (OSS vs X-Pack). type ProjectType uint8 // Project types. const ( OSSProject ProjectType = iota XPackProject CommunityProject ) // ErrUnknownProjectType is returned if an unknown ProjectType value is used. var ErrUnknownProjectType = fmt.Errorf("unknown ProjectType") // EnvMap returns map containing the common settings variables and all variables // from the environment. args are appended to the output prior to adding the // environment variables (so env vars have the highest precedence). func EnvMap(args ...map[string]interface{}) map[string]interface{} { envMap := varMap(args...) // Add the environment (highest precedence). for _, e := range os.Environ() { env := strings.SplitN(e, "=", 2) envMap[env[0]] = env[1] } return envMap } func varMap(args ...map[string]interface{}) map[string]interface{} { data := map[string]interface{}{ "GOOS": GOOS, "GOARCH": GOARCH, "GOARM": GOARM, "Platform": Platform, "PLATFORMS": PLATFORMS, "PACKAGES": PACKAGES, "BinaryExt": BinaryExt, "XPackDir": XPackDir, "BeatName": BeatName, "BeatServiceName": BeatServiceName, "BeatIndexPrefix": BeatIndexPrefix, "BeatDescription": BeatDescription, "BeatVendor": BeatVendor, "BeatLicense": BeatLicense, "BeatURL": BeatURL, "BeatUser": BeatUser, "Snapshot": Snapshot, "DEV": DevBuild, "EXTERNAL": ExternalBuild, "FIPS": FIPSBuild, "Qualifier": versionQualifier, "CI": CI, } // Add the extra args to the map. for _, m := range args { for k, v := range m { data[k] = v } } return data } func dumpVariables() (string, error) { var dumpTemplate = `## Variables GOOS = {{.GOOS}} GOARCH = {{.GOARCH}} GOARM = {{.GOARM}} Platform = {{.Platform}} BinaryExt = {{.BinaryExt}} XPackDir = {{.XPackDir}} BeatName = {{.BeatName}} BeatServiceName = {{.BeatServiceName}} BeatIndexPrefix = {{.BeatIndexPrefix}} BeatDescription = {{.BeatDescription}} BeatVendor = {{.BeatVendor}} BeatLicense = {{.BeatLicense}} BeatURL = {{.BeatURL}} BeatUser = {{.BeatUser}} VersionQualifier = {{.Qualifier}} PLATFORMS = {{.PLATFORMS}} PACKAGES = {{.PACKAGES}} CI = {{.CI}} ## Functions beat_doc_branch = {{ beat_doc_branch }} beat_version = {{ beat_version }} commit = {{ commit }} date = {{ date }} elastic_beats_dir = {{ elastic_beats_dir }} go_version = {{ go_version }} repo.RootImportPath = {{ repo.RootImportPath }} repo.CanonicalRootImportPath = {{ repo.CanonicalRootImportPath }} repo.RootDir = {{ repo.RootDir }} repo.ImportPath = {{ repo.ImportPath }} repo.SubDir = {{ repo.SubDir }} agent_package_version = {{ agent_package_version}} snapshot_suffix = {{ snapshot_suffix }} ` return Expand(dumpTemplate) } // DumpVariables writes the template variables and values to stdout. func DumpVariables() error { out, err := dumpVariables() if err != nil { return err } fmt.Println(out) return nil } var ( commitHash string commitHashOnce sync.Once ) // CommitHash returns the full length git commit hash. func CommitHash() (string, error) { var err error commitHashOnce.Do(func() { // Check commit hash override first commitHash = EnvOr(AgentCommitHashEnvVar, "") if commitHash == "" { // no override found, get the hash from HEAD commitHash, err = sh.Output("git", "rev-parse", "HEAD") } }) return commitHash, err } // CommitHashShort returns the short length git commit hash. func CommitHashShort() (string, error) { shortHash, err := CommitHash() if len(shortHash) > 6 { shortHash = shortHash[:6] } return shortHash, err } // TagContainsCommit returns true or false depending on if the current commit is part of a git tag. func TagContainsCommit() (bool, error) { commitHash, err := CommitHash() if err != nil { return false, err } out, err := sh.Output("git", "tag", "--contains", commitHash) if err != nil { return false, err } return strings.TrimSpace(out) != "", nil } func AgentPackageVersion() (string, error) { if agentPackageVersion != "" { return agentPackageVersion, nil } return BeatQualifiedVersion() } func PackageManifest(fips bool) (string, error) { packageVersion, err := AgentPackageVersion() if err != nil { return "", fmt.Errorf("retrieving agent package version: %w", err) } hash, err := CommitHash() if err != nil { return "", fmt.Errorf("retrieving agent commit hash: %w", err) } commitHashShort, err := CommitHashShort() if err != nil { return "", fmt.Errorf("retrieving agent commit hash: %w", err) } registry, err := loadFlavorsRegistry() if err != nil { return "", fmt.Errorf("retrieving agent flavors: %w", err) } return GeneratePackageManifest(BeatName, packageVersion, Snapshot, hash, commitHashShort, fips, registry) } func GeneratePackageManifest(beatName, packageVersion string, snapshot bool, fullHash, shortHash string, fips bool, flavorsRegistry map[string][]string) (string, error) { m := v1.NewManifest() m.Package.Version = packageVersion m.Package.Snapshot = snapshot m.Package.Hash = fullHash m.Package.Fips = fips versionedHomePath := path.Join("data", fmt.Sprintf("%s-%s", beatName, shortHash)) m.Package.VersionedHome = versionedHomePath m.Package.PathMappings = []map[string]string{{}} m.Package.PathMappings[0][versionedHomePath] = fmt.Sprintf("data/%s-%s%s-%s", beatName, m.Package.Version, GenerateSnapshotSuffix(snapshot), shortHash) m.Package.PathMappings[0][v1.ManifestFileName] = fmt.Sprintf("data/%s-%s%s-%s/%s", beatName, m.Package.Version, GenerateSnapshotSuffix(snapshot), shortHash, v1.ManifestFileName) m.Package.Flavors = flavorsRegistry yamlBytes, err := yaml.Marshal(m) if err != nil { return "", fmt.Errorf("marshaling manifest: %w", err) } return string(yamlBytes), nil } func SnapshotSuffix() string { return GenerateSnapshotSuffix(Snapshot) } func Substring(s string, start, length int) string { if start < 0 || start >= len(s) { return "" } end := start + length if end > len(s) { end = len(s) } return s[start:end] } func GenerateSnapshotSuffix(snapshot bool) string { if !snapshot { return "" } return "-SNAPSHOT" } var ( elasticBeatsDirValue string elasticBeatsDirErr error elasticBeatsDirLock sync.Mutex ) // SetElasticBeatsDir sets the internal elastic beats dir to a preassigned value func SetElasticBeatsDir(path string) { elasticBeatsDirLock.Lock() defer elasticBeatsDirLock.Unlock() elasticBeatsDirValue = path } // ElasticBeatsDir returns the path to Elastic beats dir. func ElasticBeatsDir() (string, error) { elasticBeatsDirLock.Lock() defer elasticBeatsDirLock.Unlock() if elasticBeatsDirValue != "" || elasticBeatsDirErr != nil { return elasticBeatsDirValue, elasticBeatsDirErr } elasticBeatsDirValue, elasticBeatsDirErr = findElasticBeatsDir() if elasticBeatsDirErr == nil { log.Println("Found Elastic Beats dir at", elasticBeatsDirValue) } return elasticBeatsDirValue, elasticBeatsDirErr } // findElasticBeatsDir returns the root directory of the Elastic Beats module, using "go list". // // When running within the Elastic Beats repo, this will return the repo root. Otherwise, // it will return the root directory of the module from within the module cache or vendor // directory. func findElasticBeatsDir() (string, error) { repo, err := GetProjectRepoInfo() if err != nil { return "", err } if repo.IsElasticBeats() { return repo.RootDir, nil } return gotool.ListModuleCacheDir(elasticAgentModulePath) } var ( buildDate = time.Now().UTC().Format(time.RFC3339) ) // BuildDate returns the time that the build started. func BuildDate() string { return buildDate } var ( goVersionValue string goVersionErr error goVersionOnce sync.Once ) // GoVersion returns the version of Go defined in the project's .go-version // file. func GoVersion() (string, error) { goVersionOnce.Do(func() { goVersionValue = os.Getenv("BEAT_GO_VERSION") if goVersionValue != "" { return } goVersionValue, goVersionErr = getBuildVariableSources().GetGoVersion() }) return goVersionValue, goVersionErr } var ( beatVersionRegex = regexp.MustCompile(`(?m)^const defaultBeatVersion = "(.+)"\r?$`) beatVersionValue string beatVersionErr error beatVersionOnce sync.Once flavorsRegistry map[string][]string flavorsRegistryErr error flavorsOnce sync.Once ) // BeatQualifiedVersion returns the Beat's qualified version. The value can be overwritten by // setting VERSION_QUALIFIER in the environment. func BeatQualifiedVersion() (string, error) { version, err := beatVersion() if err != nil { return "", err } // version qualifier can intentionally be set to "" to override build time var if !versionQualified || versionQualifier == "" { return version, nil } return version + "-" + versionQualifier, nil } // BeatVersion returns the Beat's version. The value can be overridden by // setting BEAT_VERSION in the environment. func beatVersion() (string, error) { beatVersionOnce.Do(func() { beatVersionValue = os.Getenv("BEAT_VERSION") if beatVersionValue != "" { return } beatVersionValue, beatVersionErr = getBuildVariableSources().GetBeatVersion() }) return beatVersionValue, beatVersionErr } func loadFlavorsRegistry() (map[string][]string, error) { flavorsOnce.Do(func() { flavorsRegistry, flavorsRegistryErr = getBuildVariableSources().GetFlavorsRegistry() }) return flavorsRegistry, flavorsRegistryErr } var ( beatDocBranchRegex = regexp.MustCompile(`(?m)doc-branch:\s*([^\s]+)\r?$`) beatDocSiteBranchRegex = regexp.MustCompile(`(?m)doc-site-branch:\s*([^\s]+)\r?$`) beatDocBranchValue string beatDocBranchErr error beatDocBranchOnce sync.Once ) // BeatDocBranch returns the documentation branch name associated with the // Beat branch. func BeatDocBranch() (string, error) { beatDocBranchOnce.Do(func() { beatDocBranchValue = os.Getenv("BEAT_DOC_BRANCH") if beatDocBranchValue != "" { return } beatDocBranchValue, beatDocBranchErr = getBuildVariableSources().GetDocBranch() }) return beatDocBranchValue, beatDocBranchErr } // --- BuildVariableSources var ( // DefaultBeatBuildVariableSources contains the default locations build // variables are read from by Elastic Beats. DefaultBeatBuildVariableSources = &BuildVariableSources{ BeatVersion: "{{ elastic_beats_dir }}/version/version.go", GoVersion: "{{ elastic_beats_dir }}/.go-version", DocBranch: "{{ elastic_beats_dir }}/version/docs/version.asciidoc", FlavorsRegistry: "{{ elastic_beats_dir }}/_meta/.flavors", } buildVariableSources *BuildVariableSources buildVariableSourcesLock sync.Mutex ) // SetBuildVariableSources sets the BuildVariableSources that defines where // certain build data should be sourced from. Community Beats must call this. func SetBuildVariableSources(s *BuildVariableSources) { buildVariableSourcesLock.Lock() defer buildVariableSourcesLock.Unlock() buildVariableSources = s } func getBuildVariableSources() *BuildVariableSources { buildVariableSourcesLock.Lock() defer buildVariableSourcesLock.Unlock() if buildVariableSources != nil { return buildVariableSources } repo, err := GetProjectRepoInfo() if err != nil { panic(err) } if repo.IsElasticBeats() { buildVariableSources = DefaultBeatBuildVariableSources return buildVariableSources } panic(fmt.Errorf("magefile must call devtools.SetBuildVariableSources() "+ "because it is not an elastic beat (repo=%+v)", repo.RootImportPath)) } // BuildVariableSources is used to explicitly define what files contain build // variables and how to parse the values from that file. This removes ambiguity // about where the data is sources and allows a degree of customization for // community Beats. // // Default parsers are used if one is not defined. type BuildVariableSources struct { // File containing the Beat version. BeatVersion string // Parses the Beat version from the BeatVersion file. BeatVersionParser func(data []byte) (string, error) // File containing the Go version to be used in cross-builds. GoVersion string // Parses the Go version from the GoVersion file. GoVersionParser func(data []byte) (string, error) // File containing the documentation branch. DocBranch string // Parses the documentation branch from the DocBranch file. DocBranchParser func(data []byte) (string, error) // File containing definition of flavors. FlavorsRegistry string } func (s *BuildVariableSources) expandVar(in string) (string, error) { return expandTemplate("inline", in, map[string]interface{}{ "elastic_beats_dir": ElasticBeatsDir, }) } // GetBeatVersion reads the BeatVersion file and parses the version from it. func (s *BuildVariableSources) GetBeatVersion() (string, error) { file, err := s.expandVar(s.BeatVersion) if err != nil { return "", err } data, err := os.ReadFile(file) if err != nil { return "", fmt.Errorf("failed to read beat version file=%v: %w", file, err) } if s.BeatVersionParser == nil { s.BeatVersionParser = parseBeatVersion } return s.BeatVersionParser(data) } // GetGoVersion reads the GoVersion file and parses the version from it. func (s *BuildVariableSources) GetGoVersion() (string, error) { file, err := s.expandVar(s.GoVersion) if err != nil { return "", err } data, err := os.ReadFile(file) if err != nil { return "", fmt.Errorf("failed to read go version file=%v: %w", file, err) } if s.GoVersionParser == nil { s.GoVersionParser = parseGoVersion } return s.GoVersionParser(data) } // GetFlavorsRegistry reads the flavors file and parses the list of components of it. func (s *BuildVariableSources) GetFlavorsRegistry() (map[string][]string, error) { file, err := s.expandVar(s.FlavorsRegistry) if err != nil { return nil, err } data, err := os.ReadFile(file) if err != nil { return nil, fmt.Errorf("failed to read flavors from file=%v: %w", file, err) } registry := make(map[string][]string) if err := yaml.Unmarshal(data, registry); err != nil { return nil, fmt.Errorf("failed to parse flavors: %w", err) } return registry, nil } // GetDocBranch reads the DocBranch file and parses the branch from it. func (s *BuildVariableSources) GetDocBranch() (string, error) { file, err := s.expandVar(s.DocBranch) if err != nil { return "", err } data, err := os.ReadFile(file) if err != nil { return "", fmt.Errorf("failed to read doc branch file=%v: %w", file, err) } if s.DocBranchParser == nil { s.DocBranchParser = parseDocBranch } return s.DocBranchParser(data) } func parseBeatVersion(data []byte) (string, error) { matches := beatVersionRegex.FindSubmatch(data) if len(matches) == 2 { return string(matches[1]), nil } return "", errors.New("failed to parse beat version file") } func parseGoVersion(data []byte) (string, error) { return strings.TrimSpace(string(data)), nil } func parseDocBranch(data []byte) (string, error) { matches := beatDocSiteBranchRegex.FindSubmatch(data) if len(matches) == 2 { return string(matches[1]), nil } matches = beatDocBranchRegex.FindSubmatch(data) if len(matches) == 2 { return string(matches[1]), nil } return "", errors.New("failed to parse beat doc branch") } // --- ProjectRepoInfo // ProjectRepoInfo contains information about the project's repo. type ProjectRepoInfo struct { RootImportPath string // Import path at the project root. CanonicalRootImportPath string // Pre-modules root import path (does not contain semantic import version identifier). RootDir string // Root directory of the project. ImportPath string // Import path of the current directory. SubDir string // Relative path from the root dir to the current dir. } // IsElasticBeats returns true if the current project is // github.com/elastic/beats. func (r *ProjectRepoInfo) IsElasticBeats() bool { return r.CanonicalRootImportPath == elasticAgentImportPath } var ( repoInfoValue *ProjectRepoInfo repoInfoErr error repoInfoOnce sync.Once ) // GetProjectRepoInfo returns information about the repo including the root // import path and the current directory's import path. func GetProjectRepoInfo() (*ProjectRepoInfo, error) { repoInfoOnce.Do(func() { if isUnderGOPATH() { repoInfoValue, repoInfoErr = getProjectRepoInfoUnderGopath() } else { repoInfoValue, repoInfoErr = getProjectRepoInfoWithModules() } }) return repoInfoValue, repoInfoErr } func isUnderGOPATH() bool { underGOPATH := false srcDirs, err := listSrcGOPATHs() if err != nil { return false } for _, srcDir := range srcDirs { rel, err := filepath.Rel(srcDir, CWD()) if err != nil { continue } if !strings.Contains(rel, "..") { underGOPATH = true } } return underGOPATH } func getProjectRepoInfoWithModules() (*ProjectRepoInfo, error) { var ( cwd = CWD() rootDir string subDir string ) possibleRoot := cwd var errs []string for { isRoot, err := isGoModRoot(possibleRoot) if err != nil { errs = append(errs, err.Error()) } if isRoot { rootDir = possibleRoot subDir, err = filepath.Rel(rootDir, cwd) if err != nil { errs = append(errs, err.Error()) } break } possibleRoot = filepath.Dir(possibleRoot) } if rootDir == "" { return nil, fmt.Errorf("failed to find root dir of module file: %v", errs) } rootImportPath, err := gotool.GetModuleName() if err != nil { return nil, err } return &ProjectRepoInfo{ RootImportPath: rootImportPath, CanonicalRootImportPath: filepath.ToSlash(extractCanonicalRootImportPath(rootImportPath)), RootDir: rootDir, SubDir: subDir, ImportPath: filepath.ToSlash(filepath.Join(rootImportPath, subDir)), }, nil } func isGoModRoot(path string) (bool, error) { gomodPath := filepath.Join(path, "go.mod") _, err := os.Stat(gomodPath) if os.IsNotExist(err) { return false, nil } if err != nil { return false, err } return true, nil } func getProjectRepoInfoUnderGopath() (*ProjectRepoInfo, error) { var ( cwd = CWD() errs []string rootDir string ) srcDirs, err := listSrcGOPATHs() if err != nil { return nil, err } for _, srcDir := range srcDirs { root, err := fromDir(cwd, srcDir) if err != nil { // Try the next gopath. errs = append(errs, err.Error()) continue } rootDir = filepath.Join(srcDir, root) break } if rootDir == "" { return nil, fmt.Errorf("error while determining root directory: %v", errs) } subDir, err := filepath.Rel(rootDir, cwd) if err != nil { err = errors.Unwrap(err) return nil, fmt.Errorf("failed to get relative path to repo root: %w", err) } rootImportPath, err := gotool.GetModuleName() if err != nil { return nil, err } return &ProjectRepoInfo{ RootImportPath: rootImportPath, CanonicalRootImportPath: filepath.ToSlash(extractCanonicalRootImportPath(rootImportPath)), RootDir: rootDir, SubDir: subDir, ImportPath: filepath.ToSlash(filepath.Join(rootImportPath, subDir)), }, nil } var vcsList = []string{ "hg", "git", "svn", "bzr", } func fromDir(dir, srcRoot string) (root string, err error) { // Clean and double-check that dir is in (a subdirectory of) srcRoot. dir = filepath.Clean(dir) srcRoot = filepath.Clean(srcRoot) if len(dir) <= len(srcRoot) || dir[len(srcRoot)] != filepath.Separator { return "", fmt.Errorf("directory %q is outside source root %q", dir, srcRoot) } var vcsRet string var rootRet string origDir := dir for len(dir) > len(srcRoot) { for _, vcs := range vcsList { if _, err := os.Stat(filepath.Join(dir, "."+vcs)); err == nil { root := filepath.ToSlash(dir[len(srcRoot)+1:]) // Record first VCS we find, but keep looking, // to detect mistakes like one kind of VCS inside another. if vcsRet == "" { vcsRet = vcs rootRet = root continue } // Allow .git inside .git, which can arise due to submodules. if vcsRet == vcs && vcs == "git" { continue } // Otherwise, we have one VCS inside a different VCS. return "", fmt.Errorf("directory %q uses %s, but parent %q uses %s", filepath.Join(srcRoot, rootRet), vcsRet, filepath.Join(srcRoot, root), vcs) } } // Move to parent. ndir := filepath.Dir(dir) if len(ndir) >= len(dir) { // Shouldn't happen, but just in case, stop. break } dir = ndir } if vcsRet != "" { return rootRet, nil } return "", fmt.Errorf("directory %q is not using a known version control system", origDir) } func extractCanonicalRootImportPath(rootImportPath string) string { // In order to be compatible with go modules, the root import // path of any module at major version v2 or higher must include // the major version. // Ref: https://github.com/golang/go/wiki/Modules#semantic-import-versioning // // Thus, Beats has to include the major version as well. // This regex removes the major version from the import path. re := regexp.MustCompile(`(/v[1-9][0-9]*)$`) return re.ReplaceAllString(rootImportPath, "") } func listSrcGOPATHs() ([]string, error) { var ( cwd = CWD() errs []string srcDirs []string ) for _, gopath := range filepath.SplitList(build.Default.GOPATH) { gopath = filepath.Clean(gopath) if !strings.HasPrefix(cwd, gopath) { // Fixes an issue on macOS when /var is actually /private/var. var err error gopath, err = filepath.EvalSymlinks(gopath) if err != nil { errs = append(errs, err.Error()) continue } } srcDirs = append(srcDirs, filepath.Join(gopath, "src")) } if len(srcDirs) == 0 { return srcDirs, fmt.Errorf("failed to find any GOPATH %v", errs) } return srcDirs, nil }