scripts/genmod/main.go (168 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"go.elastic.co/apm/v2"
)
var (
versionFlag = flag.String("version", "v"+apm.AgentVersion, "module version (e.g. \"v1.0.0\"")
goVersionFlag = flag.String("go", "", "go version to expect in go.mod files")
excludedPaths = flag.String("exclude", "tools", "paths to exclude. Separated by ,")
)
func init() {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s <dir>\n", os.Args[0])
flag.PrintDefaults()
}
}
func main() {
flag.Parse()
if flag.NArg() != 1 {
flag.Usage()
os.Exit(2)
}
// Locate and parse all go.mod files.
root, err := filepath.Abs(flag.Arg(0))
if err != nil {
log.Fatal(err)
}
if resolved, err := os.Readlink(root); err == nil {
root = resolved
}
paths := strings.Split(*excludedPaths, ",")
modules := make(map[string]*GoMod) // by module path
if err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
for _, p := range paths {
dir := strings.TrimPrefix(path, root+"/")
if dir == p {
fmt.Fprintf(os.Stderr, "skipping %s\n", dir)
return filepath.SkipDir
}
}
if !info.IsDir() {
if info.Name() == "go.mod" {
gomod, err := readGoMod(path)
if err != nil {
return err
}
modules[gomod.Module.Path] = gomod
}
return nil
}
if name := info.Name(); name != root && strings.HasPrefix(name, ".") {
return filepath.SkipDir
}
return nil
}); err != nil {
log.Fatal(err)
}
seen := make(map[string]bool)
var modulePaths []string
for path := range modules {
toposort(path, modules, seen, &modulePaths)
}
for _, modpath := range modulePaths {
gomod := modules[modpath]
absdir, err := filepath.Abs(gomod.dir)
if err != nil {
log.Fatal(err)
}
fmt.Fprintf(os.Stderr, "# updating %s\n", gomod.Module.Path)
if err := updateModule(absdir, gomod, modules); err != nil {
log.Fatal(err)
}
}
}
func updateModule(dir string, gomod *GoMod, modules map[string]*GoMod) error {
for _, require := range gomod.Require {
requireMod, ok := modules[require.Path]
if !ok {
continue
}
relDir, err := filepath.Rel(dir, requireMod.dir)
if err != nil {
return err
}
args := []string{
"mod", "edit",
"-require", require.Path + "@" + *versionFlag,
"-replace", require.Path + "=" + relDir,
}
cmd := exec.Command("go", args...)
cmd.Stderr = os.Stderr
cmd.Dir = dir
if err := cmd.Run(); err != nil {
return fmt.Errorf("'go mod edit' replace failed: %w", err)
}
}
cmd := exec.Command("go", "mod", "tidy", "-v", "-go", *goVersionFlag)
cmd.Stderr = os.Stderr
cmd.Dir = dir
if err := cmd.Run(); err != nil {
return fmt.Errorf("'go mod tidy' failed: %w", err)
}
cmd = exec.Command("go", "mod", "edit", "-toolchain", "none")
cmd.Stderr = os.Stderr
cmd.Dir = dir
if err := cmd.Run(); err != nil {
return fmt.Errorf("'go mod edit' toolchain failed: %w", err)
}
return nil
}
// toposort topologically sorts the required modules, starting
// with the module specified by path.
func toposort(path string, modules map[string]*GoMod, seen map[string]bool, paths *[]string) {
if seen[path] {
return
}
gomod := modules[path]
if gomod == nil {
return
}
seen[path] = true
for _, require := range gomod.Require {
toposort(require.Path, modules, seen, paths)
}
*paths = append(*paths, path)
}
func readGoMod(path string) (*GoMod, error) {
cmd := exec.Command("go", "mod", "edit", "-json", path)
cmd.Env = append(os.Environ(), "GO111MODULE=on")
cmd.Stderr = os.Stderr
output, err := cmd.Output()
if err != nil {
return nil, err
}
gomod := GoMod{dir: filepath.Dir(path)}
if err := json.Unmarshal(output, &gomod); err != nil {
return nil, err
}
return &gomod, nil
}
// GoMod holds definition of a Go module, as in go.mod.
type GoMod struct {
// dir is the directory containing go.mod
dir string
Module Module
Go string
Require []Require
Exclude []Module
Replace []Replace
}
// Require describes a "require" go.mod stanza.
type Require struct {
Path string
Version string
Indirect bool
}
// Replace describes a "replace" go.mod stanza.
type Replace struct {
Old Module
New Module
}
// Module describes a Go module path and, optionally, version.
type Module struct {
Path string
Version string
}