github.com/alexanderthaller/godep@v0.0.0-20141231210904-0baa7ea46402/save.go (about)

     1  package main
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"encoding/json"
     7  	"errors"
     8  	"io"
     9  	"io/ioutil"
    10  	"log"
    11  	"os"
    12  	"path/filepath"
    13  	"regexp"
    14  	"strings"
    15  
    16  	"github.com/kr/fs"
    17  )
    18  
    19  var cmdSave = &Command{
    20  	Usage: "save [-r] [packages]",
    21  	Short: "list and copy dependencies into Godeps",
    22  	Long: `
    23  Save writes a list of the dependencies of the named packages along
    24  with the exact source control revision of each dependency, and copies
    25  their source code into a subdirectory.
    26  
    27  The list is written to Godeps/Godeps.json, and source code for all
    28  dependencies is copied into Godeps/_workspace.
    29  
    30  The dependency list is a JSON document with the following structure:
    31  
    32  	type Godeps struct {
    33  		ImportPath string
    34  		GoVersion  string   // Abridged output of 'go version'.
    35  		Packages   []string // Arguments to godep save, if any.
    36  		Deps       []struct {
    37  			ImportPath string
    38  			Comment    string // Tag or description of commit.
    39  			Rev        string // VCS-specific commit ID.
    40  		}
    41  	}
    42  
    43  Any dependencies already present in the list will be left unchanged.
    44  To update a dependency to a newer revision, use 'godep update'.
    45  
    46  If -r is given, import statements will be rewritten to refer
    47  directly to the copied source code.
    48  
    49  For more about specifying packages, see 'go help packages'.
    50  `,
    51  	Run: runSave,
    52  }
    53  
    54  var (
    55  	saveCopy = true
    56  	saveR    = false
    57  )
    58  
    59  func init() {
    60  	cmdSave.Flag.BoolVar(&saveCopy, "copy", true, "copy source code")
    61  	cmdSave.Flag.BoolVar(&saveR, "r", false, "rewrite import paths")
    62  }
    63  
    64  func runSave(cmd *Command, args []string) {
    65  	if !saveCopy {
    66  		log.Println("flag unsupported: -copy=false")
    67  		cmd.UsageExit()
    68  	}
    69  	err := save(args)
    70  	if err != nil {
    71  		log.Fatalln(err)
    72  	}
    73  }
    74  
    75  func save(pkgs []string) error {
    76  	dot, err := LoadPackages(".")
    77  	if err != nil {
    78  		return err
    79  	}
    80  	ver, err := goVersion()
    81  	if err != nil {
    82  		return err
    83  	}
    84  	manifest := filepath.Join("Godeps", "Godeps.json")
    85  	var gold Godeps
    86  	oldIsFile, err := readOldGodeps(&gold)
    87  	if err != nil {
    88  		return err
    89  	}
    90  	gnew := &Godeps{
    91  		ImportPath: dot[0].ImportPath,
    92  		GoVersion:  ver,
    93  	}
    94  	if len(pkgs) > 0 {
    95  		gnew.Packages = pkgs
    96  	} else {
    97  		pkgs = []string{"."}
    98  	}
    99  	a, err := LoadPackages(pkgs...)
   100  	if err != nil {
   101  		return err
   102  	}
   103  	err = gnew.Load(a)
   104  	if err != nil {
   105  		return err
   106  	}
   107  	if gnew.Deps == nil {
   108  		gnew.Deps = make([]Dependency, 0) // produce json [], not null
   109  	}
   110  	gdisk := copyGodeps(gnew)
   111  	err = carryVersions(&gold, gnew)
   112  	if err != nil {
   113  		return err
   114  	}
   115  	if oldIsFile {
   116  		// If we are migrating from an old format file,
   117  		// we require that the listed version of every
   118  		// dependency must be installed in GOPATH, so it's
   119  		// available to copy.
   120  		if !eqDeps(gnew.Deps, gdisk.Deps) {
   121  			return errors.New(strings.TrimSpace(needRestore))
   122  		}
   123  		gold = Godeps{}
   124  	}
   125  	os.Remove("Godeps") // remove regular file if present; ignore error
   126  	readme := filepath.Join("Godeps", "Readme")
   127  	err = writeFile(readme, strings.TrimSpace(Readme)+"\n")
   128  	if err != nil {
   129  		log.Println(err)
   130  	}
   131  	f, err := os.Create(manifest)
   132  	if err != nil {
   133  		return err
   134  	}
   135  	_, err = gnew.WriteTo(f)
   136  	if err != nil {
   137  		f.Close()
   138  		return err
   139  	}
   140  	err = f.Close()
   141  	if err != nil {
   142  		return err
   143  	}
   144  	// We use a name starting with "_" so the go tool
   145  	// ignores this directory when traversing packages
   146  	// starting at the project's root. For example,
   147  	//   godep go list ./...
   148  	workspace := filepath.Join("Godeps", "_workspace")
   149  	srcdir := filepath.Join(workspace, "src")
   150  	rem := subDeps(gold.Deps, gnew.Deps)
   151  	add := subDeps(gnew.Deps, gold.Deps)
   152  	err = removeSrc(srcdir, rem)
   153  	if err != nil {
   154  		return err
   155  	}
   156  	err = copySrc(srcdir, add)
   157  	if err != nil {
   158  		return err
   159  	}
   160  	writeVCSIgnore(workspace)
   161  	var rewritePaths []string
   162  	if saveR {
   163  		for _, dep := range gnew.Deps {
   164  			rewritePaths = append(rewritePaths, dep.ImportPath)
   165  		}
   166  	}
   167  	return rewrite(a, dot[0].ImportPath, rewritePaths)
   168  }
   169  
   170  func readOldGodeps(g *Godeps) (isFile bool, err error) {
   171  	f, err := os.Open(filepath.Join("Godeps", "Godeps.json"))
   172  	if err != nil {
   173  		isFile = true
   174  		f, err = os.Open("Godeps")
   175  	}
   176  	if os.IsNotExist(err) {
   177  		return false, nil
   178  	}
   179  	if err != nil {
   180  		return false, err
   181  	}
   182  	err = json.NewDecoder(f).Decode(g)
   183  	f.Close()
   184  	return isFile, err
   185  }
   186  
   187  type revError struct {
   188  	ImportPath string
   189  	HaveRev    string
   190  	WantRev    string
   191  }
   192  
   193  func (v *revError) Error() string {
   194  	return v.ImportPath + ": revision is " + v.HaveRev + ", want " + v.WantRev
   195  }
   196  
   197  // carryVersions copies Rev and Comment from a to b for
   198  // each dependency with an identical ImportPath. For any
   199  // dependency in b that appears to be from the same repo
   200  // as one in a (for example, a parent or child directory),
   201  // the Rev must already match - otherwise it is an error.
   202  func carryVersions(a, b *Godeps) error {
   203  	for i := range b.Deps {
   204  		err := carryVersion(a, &b.Deps[i])
   205  		if err != nil {
   206  			return err
   207  		}
   208  	}
   209  	return nil
   210  }
   211  
   212  func carryVersion(a *Godeps, db *Dependency) error {
   213  	// First see if this exact package is already in the list.
   214  	for _, da := range a.Deps {
   215  		if db.ImportPath == da.ImportPath {
   216  			db.Rev = da.Rev
   217  			db.Comment = da.Comment
   218  			return nil
   219  		}
   220  	}
   221  	// No exact match, check for child or sibling package.
   222  	// We can't handle mismatched versions for packages in
   223  	// the same repo, so report that as an error.
   224  	for _, da := range a.Deps {
   225  		switch {
   226  		case strings.HasPrefix(db.ImportPath, da.ImportPath+"/"):
   227  			if da.Rev != db.Rev {
   228  				return &revError{db.ImportPath, db.Rev, da.Rev}
   229  			}
   230  		case strings.HasPrefix(da.ImportPath, db.root+"/"):
   231  			if da.Rev != db.Rev {
   232  				return &revError{db.ImportPath, db.Rev, da.Rev}
   233  			}
   234  		}
   235  	}
   236  	// No related package in the list, must be a new repo.
   237  	return nil
   238  }
   239  
   240  // subDeps returns a - b, using ImportPath for equality.
   241  func subDeps(a, b []Dependency) (diff []Dependency) {
   242  Diff:
   243  	for _, da := range a {
   244  		for _, db := range b {
   245  			if da.ImportPath == db.ImportPath {
   246  				continue Diff
   247  			}
   248  		}
   249  		diff = append(diff, da)
   250  	}
   251  	return diff
   252  }
   253  
   254  func removeSrc(srcdir string, deps []Dependency) error {
   255  	for _, dep := range deps {
   256  		path := filepath.FromSlash(dep.ImportPath)
   257  		err := os.RemoveAll(filepath.Join(srcdir, path))
   258  		if err != nil {
   259  			return err
   260  		}
   261  	}
   262  	return nil
   263  }
   264  
   265  func copySrc(dir string, deps []Dependency) error {
   266  	ok := true
   267  	for _, dep := range deps {
   268  		srcdir := filepath.Join(dep.ws, "src")
   269  		rel, err := filepath.Rel(srcdir, dep.dir)
   270  		if err != nil { // this should never happen
   271  			return err
   272  		}
   273  		dstpkgroot := filepath.Join(dir, rel)
   274  		err = os.RemoveAll(dstpkgroot)
   275  		if err != nil {
   276  			log.Println(err)
   277  			ok = false
   278  		}
   279  		w := fs.Walk(dep.dir)
   280  		for w.Step() {
   281  			err = copyPkgFile(dir, srcdir, w)
   282  			if err != nil {
   283  				log.Println(err)
   284  				ok = false
   285  			}
   286  		}
   287  	}
   288  	if !ok {
   289  		return errors.New("error copying source code")
   290  	}
   291  	return nil
   292  }
   293  
   294  func copyPkgFile(dstroot, srcroot string, w *fs.Walker) error {
   295  	if w.Err() != nil {
   296  		return w.Err()
   297  	}
   298  	if c := w.Stat().Name()[0]; c == '.' || c == '_' {
   299  		// Skip directories using a rule similar to how
   300  		// the go tool enumerates packages.
   301  		// See $GOROOT/src/cmd/go/main.go:/matchPackagesInFs
   302  		w.SkipDir()
   303  	}
   304  	if w.Stat().IsDir() {
   305  		return nil
   306  	}
   307  	rel, err := filepath.Rel(srcroot, w.Path())
   308  	if err != nil { // this should never happen
   309  		return err
   310  	}
   311  	return copyFile(filepath.Join(dstroot, rel), w.Path())
   312  }
   313  
   314  // copyFile copies a regular file from src to dst.
   315  // dst is opened with os.Create.
   316  // If the file name ends with .go,
   317  // copyFile strips canonical import path annotations.
   318  // These are comments of the form:
   319  //   package foo // import "bar/foo"
   320  //   package foo /* import "bar/foo" */
   321  func copyFile(dst, src string) error {
   322  	err := os.MkdirAll(filepath.Dir(dst), 0777)
   323  	if err != nil {
   324  		return err
   325  	}
   326  
   327  	linkDst, err := os.Readlink(src)
   328  	if err == nil {
   329  		return os.Symlink(linkDst, dst)
   330  	}
   331  
   332  	r, err := os.Open(src)
   333  	if err != nil {
   334  		return err
   335  	}
   336  	defer r.Close()
   337  
   338  	w, err := os.Create(dst)
   339  	if err != nil {
   340  		return err
   341  	}
   342  
   343  	if strings.HasSuffix(dst, ".go") {
   344  		err = copyWithoutImportComment(w, r)
   345  	} else {
   346  		_, err = io.Copy(w, r)
   347  	}
   348  	err1 := w.Close()
   349  	if err == nil {
   350  		err = err1
   351  	}
   352  
   353  	return err
   354  }
   355  
   356  func copyWithoutImportComment(w io.Writer, r io.Reader) error {
   357  	sc := bufio.NewScanner(r)
   358  	for sc.Scan() {
   359  		_, err := w.Write(append(stripImportComment(sc.Bytes()), '\n'))
   360  		if err != nil {
   361  			return err
   362  		}
   363  	}
   364  	return nil
   365  }
   366  
   367  const (
   368  	importAnnotation = `import\s+(?:"[^"]*"|` + "`[^`]*`" + `)`
   369  	importComment    = `(?://\s*` + importAnnotation + `\s*$|/\*\s*` + importAnnotation + `\s*\*/)`
   370  )
   371  
   372  var (
   373  	importCommentRE = regexp.MustCompile(`^\s*(package\s+\w+)\s+` + importComment + `(.*)`)
   374  	pkgPrefix       = []byte("package ")
   375  )
   376  
   377  // stripImportComment returns line with its import comment removed.
   378  // If s is not a package statement containing an import comment,
   379  // it is returned unaltered.
   380  // See also http://golang.org/s/go14customimport.
   381  func stripImportComment(line []byte) []byte {
   382  	if !bytes.HasPrefix(line, pkgPrefix) {
   383  		// Fast path; this will skip all but one line in the file.
   384  		// This assumes there is no whitespace before the keyword.
   385  		return line
   386  	}
   387  	if m := importCommentRE.FindSubmatch(line); m != nil {
   388  		return append(m[1], m[2]...)
   389  	}
   390  	return line
   391  }
   392  
   393  // Func writeVCSIgnore writes "ignore" files inside dir for known VCSs,
   394  // so that dir/pkg and dir/bin don't accidentally get committed.
   395  // It logs any errors it encounters.
   396  func writeVCSIgnore(dir string) {
   397  	// Currently git is the only VCS for which we know how to do this.
   398  	// Mercurial and Bazaar have similar mechasims, but they apparently
   399  	// require writing files outside of dir.
   400  	const ignore = "/pkg\n/bin\n"
   401  	name := filepath.Join(dir, ".gitignore")
   402  	err := writeFile(name, ignore)
   403  	if err != nil {
   404  		log.Println(err)
   405  	}
   406  }
   407  
   408  // writeFile is like ioutil.WriteFile but it creates
   409  // intermediate directories with os.MkdirAll.
   410  func writeFile(name, body string) error {
   411  	err := os.MkdirAll(filepath.Dir(name), 0777)
   412  	if err != nil {
   413  		return err
   414  	}
   415  	return ioutil.WriteFile(name, []byte(body), 0666)
   416  }
   417  
   418  const (
   419  	Readme = `
   420  This directory tree is generated automatically by godep.
   421  
   422  Please do not edit.
   423  
   424  See https://github.com/tools/godep for more information.
   425  `
   426  	needRestore = `
   427  mismatched versions while migrating
   428  
   429  It looks like you are switching from the old Godeps format
   430  (from flag -copy=false). The old format is just a file; it
   431  doesn't contain source code. For this migration, godep needs
   432  the appropriate version of each dependency to be installed in
   433  GOPATH, so that the source code is available to copy.
   434  
   435  To fix this, run 'godep restore'.
   436  `
   437  )