github.com/opsmatic/godep@v0.1.5/save.go (about)

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