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 )