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