1
2
3
4
5 package modfetch
6
7 import (
8 "bytes"
9 "context"
10 "encoding/json"
11 "errors"
12 "fmt"
13 "io"
14 "io/fs"
15 "math/rand"
16 "os"
17 "path/filepath"
18 "strconv"
19 "strings"
20 "sync"
21
22 "cmd/go/internal/base"
23 "cmd/go/internal/cfg"
24 "cmd/go/internal/gover"
25 "cmd/go/internal/lockedfile"
26 "cmd/go/internal/modfetch/codehost"
27 "cmd/go/internal/par"
28 "cmd/go/internal/robustio"
29
30 "golang.org/x/mod/module"
31 "golang.org/x/mod/semver"
32 )
33
34 func cacheDir(ctx context.Context, path string) (string, error) {
35 if err := checkCacheDir(ctx); err != nil {
36 return "", err
37 }
38 enc, err := module.EscapePath(path)
39 if err != nil {
40 return "", err
41 }
42 return filepath.Join(cfg.GOMODCACHE, "cache/download", enc, "/@v"), nil
43 }
44
45 func CachePath(ctx context.Context, m module.Version, suffix string) (string, error) {
46 if gover.IsToolchain(m.Path) {
47 return "", ErrToolchain
48 }
49 dir, err := cacheDir(ctx, m.Path)
50 if err != nil {
51 return "", err
52 }
53 if !gover.ModIsValid(m.Path, m.Version) {
54 return "", fmt.Errorf("non-semver module version %q", m.Version)
55 }
56 if module.CanonicalVersion(m.Version) != m.Version {
57 return "", fmt.Errorf("non-canonical module version %q", m.Version)
58 }
59 encVer, err := module.EscapeVersion(m.Version)
60 if err != nil {
61 return "", err
62 }
63 return filepath.Join(dir, encVer+"."+suffix), nil
64 }
65
66
67
68
69
70
71 func DownloadDir(ctx context.Context, m module.Version) (string, error) {
72 if gover.IsToolchain(m.Path) {
73 return "", ErrToolchain
74 }
75 if err := checkCacheDir(ctx); err != nil {
76 return "", err
77 }
78 enc, err := module.EscapePath(m.Path)
79 if err != nil {
80 return "", err
81 }
82 if !gover.ModIsValid(m.Path, m.Version) {
83 return "", fmt.Errorf("non-semver module version %q", m.Version)
84 }
85 if module.CanonicalVersion(m.Version) != m.Version {
86 return "", fmt.Errorf("non-canonical module version %q", m.Version)
87 }
88 encVer, err := module.EscapeVersion(m.Version)
89 if err != nil {
90 return "", err
91 }
92
93
94 dir := filepath.Join(cfg.GOMODCACHE, enc+"@"+encVer)
95 if fi, err := os.Stat(dir); os.IsNotExist(err) {
96 return dir, err
97 } else if err != nil {
98 return dir, &DownloadDirPartialError{dir, err}
99 } else if !fi.IsDir() {
100 return dir, &DownloadDirPartialError{dir, errors.New("not a directory")}
101 }
102
103
104
105 partialPath, err := CachePath(ctx, m, "partial")
106 if err != nil {
107 return dir, err
108 }
109 if _, err := os.Stat(partialPath); err == nil {
110 return dir, &DownloadDirPartialError{dir, errors.New("not completely extracted")}
111 } else if !os.IsNotExist(err) {
112 return dir, err
113 }
114
115
116
117
118
119
120 ziphashPath, err := CachePath(ctx, m, "ziphash")
121 if err != nil {
122 return dir, err
123 }
124 if _, err := os.Stat(ziphashPath); os.IsNotExist(err) {
125 return dir, &DownloadDirPartialError{dir, errors.New("ziphash file is missing")}
126 } else if err != nil {
127 return dir, err
128 }
129 return dir, nil
130 }
131
132
133
134
135
136 type DownloadDirPartialError struct {
137 Dir string
138 Err error
139 }
140
141 func (e *DownloadDirPartialError) Error() string { return fmt.Sprintf("%s: %v", e.Dir, e.Err) }
142 func (e *DownloadDirPartialError) Is(err error) bool { return err == fs.ErrNotExist }
143
144
145
146 func lockVersion(ctx context.Context, mod module.Version) (unlock func(), err error) {
147 path, err := CachePath(ctx, mod, "lock")
148 if err != nil {
149 return nil, err
150 }
151 if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
152 return nil, err
153 }
154 return lockedfile.MutexAt(path).Lock()
155 }
156
157
158
159
160
161 func SideLock(ctx context.Context) (unlock func(), err error) {
162 if err := checkCacheDir(ctx); err != nil {
163 return nil, err
164 }
165
166 path := filepath.Join(cfg.GOMODCACHE, "cache", "lock")
167 if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
168 return nil, fmt.Errorf("failed to create cache directory: %w", err)
169 }
170
171 return lockedfile.MutexAt(path).Lock()
172 }
173
174
175
176
177
178
179 type cachingRepo struct {
180 path string
181 versionsCache par.ErrCache[string, *Versions]
182 statCache par.ErrCache[string, *RevInfo]
183 latestCache par.ErrCache[struct{}, *RevInfo]
184 gomodCache par.ErrCache[string, []byte]
185
186 once sync.Once
187 initRepo func(context.Context) (Repo, error)
188 r Repo
189 }
190
191 func newCachingRepo(ctx context.Context, path string, initRepo func(context.Context) (Repo, error)) *cachingRepo {
192 return &cachingRepo{
193 path: path,
194 initRepo: initRepo,
195 }
196 }
197
198 func (r *cachingRepo) repo(ctx context.Context) Repo {
199 r.once.Do(func() {
200 var err error
201 r.r, err = r.initRepo(ctx)
202 if err != nil {
203 r.r = errRepo{r.path, err}
204 }
205 })
206 return r.r
207 }
208
209 func (r *cachingRepo) CheckReuse(ctx context.Context, old *codehost.Origin) error {
210 return r.repo(ctx).CheckReuse(ctx, old)
211 }
212
213 func (r *cachingRepo) ModulePath() string {
214 return r.path
215 }
216
217 func (r *cachingRepo) Versions(ctx context.Context, prefix string) (*Versions, error) {
218 v, err := r.versionsCache.Do(prefix, func() (*Versions, error) {
219 return r.repo(ctx).Versions(ctx, prefix)
220 })
221
222 if err != nil {
223 return nil, err
224 }
225 return &Versions{
226 Origin: v.Origin,
227 List: append([]string(nil), v.List...),
228 }, nil
229 }
230
231 type cachedInfo struct {
232 info *RevInfo
233 err error
234 }
235
236 func (r *cachingRepo) Stat(ctx context.Context, rev string) (*RevInfo, error) {
237 if gover.IsToolchain(r.path) {
238
239 return r.repo(ctx).Stat(ctx, rev)
240 }
241 info, err := r.statCache.Do(rev, func() (*RevInfo, error) {
242 file, info, err := readDiskStat(ctx, r.path, rev)
243 if err == nil {
244 return info, err
245 }
246
247 info, err = r.repo(ctx).Stat(ctx, rev)
248 if err == nil {
249
250
251 if info.Version != rev {
252 file, _ = CachePath(ctx, module.Version{Path: r.path, Version: info.Version}, "info")
253 r.statCache.Do(info.Version, func() (*RevInfo, error) {
254 return info, nil
255 })
256 }
257
258 if err := writeDiskStat(ctx, file, info); err != nil {
259 fmt.Fprintf(os.Stderr, "go: writing stat cache: %v\n", err)
260 }
261 }
262 return info, err
263 })
264 if info != nil {
265 copy := *info
266 info = ©
267 }
268 return info, err
269 }
270
271 func (r *cachingRepo) Latest(ctx context.Context) (*RevInfo, error) {
272 if gover.IsToolchain(r.path) {
273
274 return r.repo(ctx).Latest(ctx)
275 }
276 info, err := r.latestCache.Do(struct{}{}, func() (*RevInfo, error) {
277 info, err := r.repo(ctx).Latest(ctx)
278
279
280 if err == nil {
281 r.statCache.Do(info.Version, func() (*RevInfo, error) {
282 return info, nil
283 })
284 if file, _, err := readDiskStat(ctx, r.path, info.Version); err != nil {
285 writeDiskStat(ctx, file, info)
286 }
287 }
288
289 return info, err
290 })
291 if info != nil {
292 copy := *info
293 info = ©
294 }
295 return info, err
296 }
297
298 func (r *cachingRepo) GoMod(ctx context.Context, version string) ([]byte, error) {
299 if gover.IsToolchain(r.path) {
300
301 return r.repo(ctx).GoMod(ctx, version)
302 }
303 text, err := r.gomodCache.Do(version, func() ([]byte, error) {
304 file, text, err := readDiskGoMod(ctx, r.path, version)
305 if err == nil {
306
307 return text, nil
308 }
309
310 text, err = r.repo(ctx).GoMod(ctx, version)
311 if err == nil {
312 if err := checkGoMod(r.path, version, text); err != nil {
313 return text, err
314 }
315 if err := writeDiskGoMod(ctx, file, text); err != nil {
316 fmt.Fprintf(os.Stderr, "go: writing go.mod cache: %v\n", err)
317 }
318 }
319 return text, err
320 })
321 if err != nil {
322 return nil, err
323 }
324 return append([]byte(nil), text...), nil
325 }
326
327 func (r *cachingRepo) Zip(ctx context.Context, dst io.Writer, version string) error {
328 if gover.IsToolchain(r.path) {
329 return ErrToolchain
330 }
331 return r.repo(ctx).Zip(ctx, dst, version)
332 }
333
334
335
336 func InfoFile(ctx context.Context, path, version string) (*RevInfo, string, error) {
337 if !gover.ModIsValid(path, version) {
338 return nil, "", fmt.Errorf("invalid version %q", version)
339 }
340
341 if file, info, err := readDiskStat(ctx, path, version); err == nil {
342 return info, file, nil
343 }
344
345 var info *RevInfo
346 var err2info map[error]*RevInfo
347 err := TryProxies(func(proxy string) error {
348 i, err := Lookup(ctx, proxy, path).Stat(ctx, version)
349 if err == nil {
350 info = i
351 } else {
352 if err2info == nil {
353 err2info = make(map[error]*RevInfo)
354 }
355 err2info[err] = info
356 }
357 return err
358 })
359 if err != nil {
360 return err2info[err], "", err
361 }
362
363
364 file, err := CachePath(ctx, module.Version{Path: path, Version: version}, "info")
365 if err != nil {
366 return nil, "", err
367 }
368 return info, file, nil
369 }
370
371
372
373
374 func GoMod(ctx context.Context, path, rev string) ([]byte, error) {
375
376
377 if !gover.ModIsValid(path, rev) {
378 if _, info, err := readDiskStat(ctx, path, rev); err == nil {
379 rev = info.Version
380 } else {
381 if errors.Is(err, statCacheErr) {
382 return nil, err
383 }
384 err := TryProxies(func(proxy string) error {
385 info, err := Lookup(ctx, proxy, path).Stat(ctx, rev)
386 if err == nil {
387 rev = info.Version
388 }
389 return err
390 })
391 if err != nil {
392 return nil, err
393 }
394 }
395 }
396
397 _, data, err := readDiskGoMod(ctx, path, rev)
398 if err == nil {
399 return data, nil
400 }
401
402 err = TryProxies(func(proxy string) (err error) {
403 data, err = Lookup(ctx, proxy, path).GoMod(ctx, rev)
404 return err
405 })
406 return data, err
407 }
408
409
410
411 func GoModFile(ctx context.Context, path, version string) (string, error) {
412 if !gover.ModIsValid(path, version) {
413 return "", fmt.Errorf("invalid version %q", version)
414 }
415 if _, err := GoMod(ctx, path, version); err != nil {
416 return "", err
417 }
418
419 file, err := CachePath(ctx, module.Version{Path: path, Version: version}, "mod")
420 if err != nil {
421 return "", err
422 }
423 return file, nil
424 }
425
426
427
428 func GoModSum(ctx context.Context, path, version string) (string, error) {
429 if !gover.ModIsValid(path, version) {
430 return "", fmt.Errorf("invalid version %q", version)
431 }
432 data, err := GoMod(ctx, path, version)
433 if err != nil {
434 return "", err
435 }
436 sum, err := goModSum(data)
437 if err != nil {
438 return "", err
439 }
440 return sum, nil
441 }
442
443 var errNotCached = fmt.Errorf("not in cache")
444
445
446
447
448
449 func readDiskStat(ctx context.Context, path, rev string) (file string, info *RevInfo, err error) {
450 if gover.IsToolchain(path) {
451 return "", nil, errNotCached
452 }
453 file, data, err := readDiskCache(ctx, path, rev, "info")
454 if err != nil {
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474 if cfg.GOPROXY == "off" {
475 if file, info, err := readDiskStatByHash(ctx, path, rev); err == nil {
476 return file, info, nil
477 }
478 }
479 return file, nil, err
480 }
481 info = new(RevInfo)
482 if err := json.Unmarshal(data, info); err != nil {
483 return file, nil, errNotCached
484 }
485
486
487
488 data2, err := json.Marshal(info)
489 if err == nil && !bytes.Equal(data2, data) {
490 writeDiskCache(ctx, file, data)
491 }
492 return file, info, nil
493 }
494
495
496
497
498
499
500
501
502
503
504 func readDiskStatByHash(ctx context.Context, path, rev string) (file string, info *RevInfo, err error) {
505 if gover.IsToolchain(path) {
506 return "", nil, errNotCached
507 }
508 if cfg.GOMODCACHE == "" {
509
510 return "", nil, errNotCached
511 }
512
513 if !codehost.AllHex(rev) || len(rev) < 12 {
514 return "", nil, errNotCached
515 }
516 rev = rev[:12]
517 cdir, err := cacheDir(ctx, path)
518 if err != nil {
519 return "", nil, errNotCached
520 }
521 dir, err := os.Open(cdir)
522 if err != nil {
523 return "", nil, errNotCached
524 }
525 names, err := dir.Readdirnames(-1)
526 dir.Close()
527 if err != nil {
528 return "", nil, errNotCached
529 }
530
531
532
533
534 var maxVersion string
535 suffix := "-" + rev + ".info"
536 err = errNotCached
537 for _, name := range names {
538 if strings.HasSuffix(name, suffix) {
539 v := strings.TrimSuffix(name, ".info")
540 if module.IsPseudoVersion(v) && semver.Compare(v, maxVersion) > 0 {
541 maxVersion = v
542 file, info, err = readDiskStat(ctx, path, strings.TrimSuffix(name, ".info"))
543 }
544 }
545 }
546 return file, info, err
547 }
548
549
550
551
552
553
554 var oldVgoPrefix = []byte("//vgo 0.0.")
555
556
557
558
559
560 func readDiskGoMod(ctx context.Context, path, rev string) (file string, data []byte, err error) {
561 if gover.IsToolchain(path) {
562 return "", nil, errNotCached
563 }
564 file, data, err = readDiskCache(ctx, path, rev, "mod")
565
566
567 if bytes.HasPrefix(data, oldVgoPrefix) {
568 err = errNotCached
569 data = nil
570 }
571
572 if err == nil {
573 if err := checkGoMod(path, rev, data); err != nil {
574 return "", nil, err
575 }
576 }
577
578 return file, data, err
579 }
580
581
582
583
584
585
586 func readDiskCache(ctx context.Context, path, rev, suffix string) (file string, data []byte, err error) {
587 if gover.IsToolchain(path) {
588 return "", nil, errNotCached
589 }
590 file, err = CachePath(ctx, module.Version{Path: path, Version: rev}, suffix)
591 if err != nil {
592 return "", nil, errNotCached
593 }
594 data, err = robustio.ReadFile(file)
595 if err != nil {
596 return file, nil, errNotCached
597 }
598 return file, data, nil
599 }
600
601
602
603 func writeDiskStat(ctx context.Context, file string, info *RevInfo) error {
604 if file == "" {
605 return nil
606 }
607
608 if info.Origin != nil {
609
610
611
612 clean := *info
613 info = &clean
614 o := *info.Origin
615 info.Origin = &o
616
617
618
619 o.TagSum = ""
620 o.TagPrefix = ""
621
622 if module.IsPseudoVersion(info.Version) {
623 o.Ref = ""
624 }
625 }
626
627 js, err := json.Marshal(info)
628 if err != nil {
629 return err
630 }
631 return writeDiskCache(ctx, file, js)
632 }
633
634
635
636 func writeDiskGoMod(ctx context.Context, file string, text []byte) error {
637 return writeDiskCache(ctx, file, text)
638 }
639
640
641
642 func writeDiskCache(ctx context.Context, file string, data []byte) error {
643 if file == "" {
644 return nil
645 }
646
647 if err := os.MkdirAll(filepath.Dir(file), 0777); err != nil {
648 return err
649 }
650
651
652
653 f, err := tempFile(ctx, filepath.Dir(file), filepath.Base(file), 0666)
654 if err != nil {
655 return err
656 }
657 defer func() {
658
659
660
661 if err != nil {
662 f.Close()
663 os.Remove(f.Name())
664 }
665 }()
666
667 if _, err := f.Write(data); err != nil {
668 return err
669 }
670 if err := f.Close(); err != nil {
671 return err
672 }
673 if err := robustio.Rename(f.Name(), file); err != nil {
674 return err
675 }
676
677 if strings.HasSuffix(file, ".mod") {
678 rewriteVersionList(ctx, filepath.Dir(file))
679 }
680 return nil
681 }
682
683
684 func tempFile(ctx context.Context, dir, prefix string, perm fs.FileMode) (f *os.File, err error) {
685 for i := 0; i < 10000; i++ {
686 name := filepath.Join(dir, prefix+strconv.Itoa(rand.Intn(1000000000))+".tmp")
687 f, err = os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, perm)
688 if os.IsExist(err) {
689 if ctx.Err() != nil {
690 return nil, ctx.Err()
691 }
692 continue
693 }
694 break
695 }
696 return
697 }
698
699
700
701 func rewriteVersionList(ctx context.Context, dir string) (err error) {
702 if filepath.Base(dir) != "@v" {
703 base.Fatalf("go: internal error: misuse of rewriteVersionList")
704 }
705
706 listFile := filepath.Join(dir, "list")
707
708
709
710
711
712
713
714
715
716
717 f, err := lockedfile.Edit(listFile)
718 if err != nil {
719 return err
720 }
721 defer func() {
722 if cerr := f.Close(); cerr != nil && err == nil {
723 err = cerr
724 }
725 }()
726 infos, err := os.ReadDir(dir)
727 if err != nil {
728 return err
729 }
730 var list []string
731 for _, info := range infos {
732
733
734
735
736
737
738 name := info.Name()
739 if v, found := strings.CutSuffix(name, ".mod"); found {
740 if v != "" && module.CanonicalVersion(v) == v {
741 list = append(list, v)
742 }
743 }
744 }
745 semver.Sort(list)
746
747 var buf bytes.Buffer
748 for _, v := range list {
749 buf.WriteString(v)
750 buf.WriteString("\n")
751 }
752 if fi, err := f.Stat(); err == nil && int(fi.Size()) == buf.Len() {
753 old := make([]byte, buf.Len()+1)
754 if n, err := f.ReadAt(old, 0); err == io.EOF && n == buf.Len() && bytes.Equal(buf.Bytes(), old) {
755 return nil
756 }
757 }
758
759
760 if err := f.Truncate(0); err != nil {
761 return err
762 }
763
764 if err := f.Truncate(int64(buf.Len())); err != nil {
765 return err
766 }
767
768
769 if _, err := f.Write(buf.Bytes()); err != nil {
770 f.Truncate(0)
771 return err
772 }
773
774 return nil
775 }
776
777 var (
778 statCacheOnce sync.Once
779 statCacheErr error
780 )
781
782
783
784 func checkCacheDir(ctx context.Context) error {
785 if cfg.GOMODCACHE == "" {
786
787
788 return fmt.Errorf("module cache not found: neither GOMODCACHE nor GOPATH is set")
789 }
790 if !filepath.IsAbs(cfg.GOMODCACHE) {
791 return fmt.Errorf("GOMODCACHE entry is relative; must be absolute path: %q.\n", cfg.GOMODCACHE)
792 }
793
794
795
796 statCacheOnce.Do(func() {
797 fi, err := os.Stat(cfg.GOMODCACHE)
798 if err != nil {
799 if !os.IsNotExist(err) {
800 statCacheErr = fmt.Errorf("could not create module cache: %w", err)
801 return
802 }
803 if err := os.MkdirAll(cfg.GOMODCACHE, 0777); err != nil {
804 statCacheErr = fmt.Errorf("could not create module cache: %w", err)
805 return
806 }
807 return
808 }
809 if !fi.IsDir() {
810 statCacheErr = fmt.Errorf("could not create module cache: %q is not a directory", cfg.GOMODCACHE)
811 return
812 }
813 })
814 return statCacheErr
815 }
816
View as plain text