github.com/elubow/godep@v0.0.0-20140525002653-983ff9241cea/save.go (about)

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