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