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 )