github.com/meatballhat/deppy@v0.0.0-20151116212532-116c2a9aa48d/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 Deps",
    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 to a file
    22  named "Deps".
    23  
    24  The dependency list is a JSON document with the following structure:
    25  
    26  	type Deps struct {
    27  		ImportPath string
    28  		GoVersion  string   // Abridged output of 'go version'.
    29  		Packages   []string // Arguments to deppy 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  
    39  For more about specifying packages, see 'go help packages'.
    40  `,
    41  	Run: runSave,
    42  }
    43  
    44  var (
    45  	saveCopy = true
    46  )
    47  
    48  func init() {
    49  	cmdSave.Flag.BoolVar(&saveCopy, "copy", false, "copy source code")
    50  }
    51  
    52  func runSave(cmd *Command, args []string) {
    53  	err := save(args)
    54  	if err != nil {
    55  		log.Fatalln(err)
    56  	}
    57  }
    58  
    59  func save(pkgs []string) error {
    60  	if saveCopy {
    61  		log.Println(strings.TrimSpace(copyWarning))
    62  	}
    63  	dot, err := LoadPackages(".")
    64  	if err != nil {
    65  		return err
    66  	}
    67  	ver, err := goVersion()
    68  	if err != nil {
    69  		return err
    70  	}
    71  	manifest := "Deps"
    72  	var gold Deps
    73  	gnew := &Deps{
    74  		ImportPath: dot[0].ImportPath,
    75  		GoVersion:  ver,
    76  	}
    77  	if len(pkgs) > 0 {
    78  		gnew.Packages = pkgs
    79  	} else {
    80  		pkgs = []string{"."}
    81  	}
    82  	a, err := LoadPackages(pkgs...)
    83  	if err != nil {
    84  		return err
    85  	}
    86  	err = gnew.Load(a)
    87  	if err != nil {
    88  		return err
    89  	}
    90  	if a := badSandboxVCS(gnew.Deps); a != nil {
    91  		log.Println("Unsupported sandbox VCS:", strings.Join(a, ", "))
    92  		return errors.New("error")
    93  	}
    94  	if gnew.Deps == nil {
    95  		gnew.Deps = make([]Dependency, 0) // produce json [], not null
    96  	}
    97  	err = carryVersions(&gold, gnew)
    98  	if err != nil {
    99  		return err
   100  	}
   101  	err = os.RemoveAll("Deps")
   102  	if err != nil {
   103  		log.Println(err)
   104  	}
   105  	f, err := os.Create(manifest)
   106  	if err != nil {
   107  		return err
   108  	}
   109  	_, err = gnew.WriteTo(f)
   110  	if err != nil {
   111  		return err
   112  	}
   113  	err = f.Close()
   114  	if err != nil {
   115  		return err
   116  	}
   117  	var rewritePaths []string
   118  	return rewrite(a, dot[0].ImportPath, rewritePaths)
   119  }
   120  
   121  type revError struct {
   122  	ImportPath string
   123  	HaveRev    string
   124  	WantRev    string
   125  }
   126  
   127  func (v *revError) Error() string {
   128  	return v.ImportPath + ": revision is " + v.HaveRev + ", want " + v.WantRev
   129  }
   130  
   131  // carryVersions copies Rev and Comment from a to b for
   132  // each dependency with an identical ImportPath. For any
   133  // dependency in b that appears to be from the same repo
   134  // as one in a (for example, a parent or child directory),
   135  // the Rev must already match - otherwise it is an error.
   136  func carryVersions(a, b *Deps) error {
   137  	for i := range b.Deps {
   138  		err := carryVersion(a, &b.Deps[i])
   139  		if err != nil {
   140  			return err
   141  		}
   142  	}
   143  	return nil
   144  }
   145  
   146  func carryVersion(a *Deps, db *Dependency) error {
   147  	// First see if this exact package is already in the list.
   148  	for _, da := range a.Deps {
   149  		if db.ImportPath == da.ImportPath {
   150  			db.Rev = da.Rev
   151  			db.Comment = da.Comment
   152  			return nil
   153  		}
   154  	}
   155  	// No exact match, check for child or sibling package.
   156  	// We can't handle mismatched versions for packages in
   157  	// the same repo, so report that as an error.
   158  	for _, da := range a.Deps {
   159  		switch {
   160  		case strings.HasPrefix(db.ImportPath, da.ImportPath+"/"):
   161  			if da.Rev != db.Rev {
   162  				return &revError{db.ImportPath, db.Rev, da.Rev}
   163  			}
   164  		case strings.HasPrefix(da.ImportPath, db.root+"/"):
   165  			if da.Rev != db.Rev {
   166  				return &revError{db.ImportPath, db.Rev, da.Rev}
   167  			}
   168  		}
   169  	}
   170  	// No related package in the list, must be a new repo.
   171  	return nil
   172  }
   173  
   174  // subDeps returns a - b, using ImportPath for equality.
   175  func subDeps(a, b []Dependency) (diff []Dependency) {
   176  Diff:
   177  	for _, da := range a {
   178  		for _, db := range b {
   179  			if da.ImportPath == db.ImportPath {
   180  				continue Diff
   181  			}
   182  		}
   183  		diff = append(diff, da)
   184  	}
   185  	return diff
   186  }
   187  
   188  // badSandboxVCS returns a list of VCSes that don't work
   189  // with the `deppy go` sandbox code.
   190  func badSandboxVCS(deps []Dependency) (a []string) {
   191  	for _, d := range deps {
   192  		if d.vcs.CreateCmd == "" {
   193  			a = append(a, d.vcs.vcs.Name)
   194  		}
   195  	}
   196  	sort.Strings(a)
   197  	return uniq(a)
   198  }
   199  
   200  func removeSrc(srcdir string, deps []Dependency) error {
   201  	for _, dep := range deps {
   202  		path := filepath.FromSlash(dep.ImportPath)
   203  		err := os.RemoveAll(filepath.Join(srcdir, path))
   204  		if err != nil {
   205  			return err
   206  		}
   207  	}
   208  	return nil
   209  }
   210  
   211  func copySrc(dir string, deps []Dependency) error {
   212  	ok := true
   213  	for _, dep := range deps {
   214  		srcdir := filepath.Join(dep.ws, "src")
   215  		rel, err := filepath.Rel(srcdir, dep.dir)
   216  		if err != nil { // this should never happen
   217  			return err
   218  		}
   219  		dstpkgroot := filepath.Join(dir, rel)
   220  		err = os.RemoveAll(dstpkgroot)
   221  		if err != nil {
   222  			log.Println(err)
   223  			ok = false
   224  		}
   225  		w := fs.Walk(dep.dir)
   226  		for w.Step() {
   227  			err = copyPkgFile(dir, srcdir, w)
   228  			if err != nil {
   229  				log.Println(err)
   230  				ok = false
   231  			}
   232  		}
   233  	}
   234  	if !ok {
   235  		return errors.New("error copying source code")
   236  	}
   237  	return nil
   238  }
   239  
   240  func copyPkgFile(dstroot, srcroot string, w *fs.Walker) error {
   241  	if w.Err() != nil {
   242  		return w.Err()
   243  	}
   244  	if c := w.Stat().Name()[0]; c == '.' || c == '_' {
   245  		// Skip directories using a rule similar to how
   246  		// the go tool enumerates packages.
   247  		// See $GOROOT/src/cmd/go/main.go:/matchPackagesInFs
   248  		w.SkipDir()
   249  	}
   250  	if w.Stat().IsDir() {
   251  		return nil
   252  	}
   253  	rel, err := filepath.Rel(srcroot, w.Path())
   254  	if err != nil { // this should never happen
   255  		return err
   256  	}
   257  	return copyFile(filepath.Join(dstroot, rel), w.Path())
   258  }
   259  
   260  // copyFile copies a regular file from src to dst.
   261  // dst is opened with os.Create.
   262  func copyFile(dst, src string) error {
   263  	err := os.MkdirAll(filepath.Dir(dst), 0777)
   264  	if err != nil {
   265  		return err
   266  	}
   267  
   268  	linkDst, err := os.Readlink(src)
   269  	if err == nil {
   270  		return os.Symlink(linkDst, dst)
   271  	}
   272  
   273  	r, err := os.Open(src)
   274  	if err != nil {
   275  		return err
   276  	}
   277  	defer r.Close()
   278  
   279  	w, err := os.Create(dst)
   280  	if err != nil {
   281  		return err
   282  	}
   283  
   284  	_, err = io.Copy(w, r)
   285  	err1 := w.Close()
   286  	if err == nil {
   287  		err = err1
   288  	}
   289  
   290  	return err
   291  }
   292  
   293  // Func writeVCSIgnore writes "ignore" files inside dir for known VCSs,
   294  // so that dir/pkg and dir/bin don't accidentally get committed.
   295  // It logs any errors it encounters.
   296  func writeVCSIgnore(dir string) {
   297  	// Currently git is the only VCS for which we know how to do this.
   298  	// Mercurial and Bazaar have similar mechasims, but they apparently
   299  	// require writing files outside of dir.
   300  	const ignore = "/pkg\n/bin\n"
   301  	name := filepath.Join(dir, ".gitignore")
   302  	err := writeFile(name, ignore)
   303  	if err != nil {
   304  		log.Println(err)
   305  	}
   306  }
   307  
   308  // writeFile is like ioutil.WriteFile but it creates
   309  // intermediate directories with os.MkdirAll.
   310  func writeFile(name, body string) error {
   311  	err := os.MkdirAll(filepath.Dir(name), 0777)
   312  	if err != nil {
   313  		return err
   314  	}
   315  	return ioutil.WriteFile(name, []byte(body), 0666)
   316  }
   317  
   318  const (
   319  	copyWarning = `
   320  deprecated flag -copy=true
   321  
   322  The flag -copy=true does not exist.  It's just gone.  Wow!
   323  `
   324  )