blob: 410fce23d15e36ccc1253bbbf2f2a89b09adf1b0 [file] [log] [blame]
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The gitlock command helps write Dockerfiles with a bunch of lines
// to lock git dependencies in place.
package main
import (
"bufio"
"bytes"
"flag"
"fmt"
"go/build"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
)
var (
ignorePrefixFlag = flag.String("ignore", "golang.org/x/build", "comma-separated list of package prefixes to ignore")
updateFile = flag.String("update", "", "if non-empty, the Dockerfile to update. must have \"# BEGIN deps\" and \"# END deps\" lines.")
tags = flag.String("tags", "", "space-separated tags to pass on to 'go list -tags=XXX target'")
)
var ignorePrefixes []string
func parseDockerFile() (header, footer []byte) {
if *updateFile == "" {
return nil, nil
}
var headBuf, footBuf bytes.Buffer
slurp, err := ioutil.ReadFile(*updateFile)
if err != nil {
log.Fatal(err)
}
var sawBegin, sawEnd bool
bs := bufio.NewScanner(bytes.NewReader(slurp))
for bs.Scan() {
if !sawBegin {
headBuf.Write(bs.Bytes())
headBuf.WriteByte('\n')
if strings.HasPrefix(bs.Text(), "# BEGIN deps") {
sawBegin = true
continue
}
continue
}
if strings.HasPrefix(bs.Text(), "# END deps") {
sawEnd = true
}
if sawEnd {
footBuf.Write(bs.Bytes())
footBuf.WriteByte('\n')
}
}
if err := bs.Err(); err != nil {
log.Fatalf("error parsing %s: %v", *updateFile, err)
}
if !sawBegin {
log.Fatalf(`file %s is missing a "# BEGIN deps" line`, *updateFile)
}
if !sawEnd {
log.Fatalf(`file %s is missing a "# END deps" line`, *updateFile)
}
return headBuf.Bytes(), footBuf.Bytes()
}
func main() {
flag.Parse()
ignorePrefixes = strings.Split(*ignorePrefixFlag, ",")
if flag.NArg() != 1 {
log.SetFlags(0)
log.Fatalf("Usage: gitlock <package>")
}
mainPkg := flag.Arg(0) // not necessary "package main", but almost certainly.
header, footer := parseDockerFile()
depOut, err := exec.Command("go", "list",
"-tags="+*tags,
"-f", "{{range .Deps}}{{.}}\n{{end}}", mainPkg).Output()
if err != nil {
log.Fatalf("listing deps of %q: %v", mainPkg, formatExecErr(err))
}
var deps []string
for _, pkg := range strings.Split(string(depOut), "\n") {
if ignorePkg(pkg) {
continue
}
deps = append(deps, pkg)
}
sort.Strings(deps)
// Build a map of root git dir => packages using that root git dir.
var gitDirPkgs = map[string][]string{}
for _, pkg := range deps {
buildPkg, err := build.Import(pkg, "", build.FindOnly)
if err != nil {
log.Fatalf("importing %s: %v", pkg, err)
}
pkgDir := buildPkg.Dir
gitDir, err := findGitDir(pkgDir)
if err != nil {
log.Fatalf("finding git dir of %s: %v", pkgDir, err)
}
gitDirPkgs[gitDir] = append(gitDirPkgs[gitDir], pkg)
}
// Sorted list of unique git root dirs.
var gitDirs []string
for d := range gitDirPkgs {
gitDirs = append(gitDirs, d)
}
sort.Strings(gitDirs)
var buf bytes.Buffer
var out io.Writer = os.Stdout
if *updateFile != "" {
buf.Write(header)
buf.WriteByte('\n')
out = &buf
}
for _, gitDir := range gitDirs {
cmd := exec.Command("git", "log", "-n", "1", "--pretty=%H %ci")
cmd.Dir = gitDir
stdout, err := cmd.Output()
if err != nil {
log.Fatal(err)
}
f := strings.SplitN(strings.TrimSpace(string(stdout)), " ", 2)
hash, ymd := f[0], f[1][:10]
repoName := gitDir[strings.Index(gitDir, "/src/")+5:]
var comment string
if n := len(gitDirPkgs[gitDir]); n > 1 {
comment = fmt.Sprintf(" `#and %d other pkgs`", n)
}
fmt.Fprintf(out, "# Repo %s at %s (%s)\n", repoName, hash[:7], ymd)
fmt.Fprintf(out, "ENV REV=%s\n", hash)
fmt.Fprintf(out, "RUN go get -d %s%s &&\\\n", gitDirPkgs[gitDir][0], comment)
fmt.Fprintf(out, " (cd /go/src/%s && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV)\n\n", repoName)
}
fmt.Fprintf(out, "# Optimization to speed up iterative development, not necessary for correctness:\n")
fmt.Fprintf(out, "RUN go install %s\n", strings.Join(deps, " \\\n\t"))
if *updateFile == "" {
return
}
buf.Write(footer)
if err := ioutil.WriteFile(*updateFile, buf.Bytes(), 0644); err != nil {
log.Fatal(err)
}
}
func findGitDir(dir string) (string, error) {
dir0 := dir
var lastDir string
for dir != "" && dir != "." && dir != lastDir {
fi, err := os.Stat(filepath.Join(dir, ".git"))
if err == nil && fi.IsDir() {
return dir, nil
}
if err != nil && !os.IsNotExist(err) {
return "", err
}
lastDir = dir
dir = filepath.Dir(dir)
}
return "", fmt.Errorf("no git dir found for %s", dir0)
}
// ignorePkg reports whether pkg, if it appears as a dependency of the
// root package, should be omitted from the output.
func ignorePkg(pkg string) bool {
if strings.HasPrefix(pkg, "vendor/") || !strings.Contains(pkg, ".") {
return true
}
for _, pfx := range ignorePrefixes {
if strings.HasPrefix(pkg, pfx) {
return true
}
}
return false
}
func formatExecErr(err error) string {
if ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 {
return fmt.Sprintf("%s: %s", err, ee.Stderr)
}
return fmt.Sprint(err)
}