vendor.go raw
1 package loader
2
3 import (
4 "crypto/sha256"
5 "encoding/hex"
6 "fmt"
7 "os"
8 "path/filepath"
9 "sort"
10 "strings"
11 )
12
13 // Vendor copies all required dependencies into the vendor/ directory
14 // under the module root. It reads requires and replaces from the module
15 // file, resolves each to the module cache, and copies the source files.
16 // A vendor/modules.txt is written for compatibility with Go tooling.
17 // A vendor/moxie.sum is written with SHA256 hashes of each vendored file.
18 func Vendor(dir string) error {
19 modPath, modDir, _, err := readModInfo(dir)
20 if err != nil {
21 return fmt.Errorf("vendor: %w", err)
22 }
23 if modPath == "" {
24 return fmt.Errorf("vendor: no module file found in %s or parents", dir)
25 }
26
27 modFile := findModFile(modDir)
28 requires, err := readModRequires(modFile)
29 if err != nil {
30 return fmt.Errorf("vendor: %w", err)
31 }
32 replaces, err := readModReplaces(modFile, modDir)
33 if err != nil {
34 return fmt.Errorf("vendor: %w", err)
35 }
36
37 if len(requires) == 0 && len(replaces) == 0 {
38 fmt.Println("no dependencies to vendor")
39 return nil
40 }
41
42 modCache := gomodcache()
43
44 vendorDir := filepath.Join(modDir, "vendor")
45 if err := os.RemoveAll(vendorDir); err != nil {
46 return fmt.Errorf("vendor: cleaning vendor dir: %w", err)
47 }
48 if err := os.MkdirAll(vendorDir, 0755); err != nil {
49 return fmt.Errorf("vendor: creating vendor dir: %w", err)
50 }
51
52 var modulesTxt strings.Builder
53 var sumLines []string
54
55 mods := make([]string, 0, len(requires))
56 for mod := range requires {
57 mods = append(mods, mod)
58 }
59 sort.Strings(mods)
60
61 for _, mod := range mods {
62 ver := requires[mod]
63
64 var srcDir string
65 if localDir, ok := replaces[mod]; ok {
66 srcDir = localDir
67 } else {
68 srcDir = filepath.Join(modCache, mod+"@"+ver)
69 }
70
71 if !isDir(srcDir) {
72 return fmt.Errorf("vendor: source not found for %s@%s at %s", mod, ver, srcDir)
73 }
74
75 dstDir := filepath.Join(vendorDir, mod)
76
77 hashes, err := copyTree(srcDir, dstDir)
78 if err != nil {
79 return fmt.Errorf("vendor: copying %s: %w", mod, err)
80 }
81
82 modulesTxt.WriteString(fmt.Sprintf("# %s %s\n## explicit\n", mod, ver))
83
84 for _, h := range hashes {
85 sumLines = append(sumLines, fmt.Sprintf("%s %s", h.hash, h.path))
86 }
87 }
88
89 if err := os.WriteFile(filepath.Join(vendorDir, "modules.txt"), []byte(modulesTxt.String()), 0644); err != nil {
90 return fmt.Errorf("vendor: writing modules.txt: %w", err)
91 }
92
93 sort.Strings(sumLines)
94 sumContent := strings.Join(sumLines, "\n") + "\n"
95 if err := os.WriteFile(filepath.Join(vendorDir, "moxie.sum"), []byte(sumContent), 0644); err != nil {
96 return fmt.Errorf("vendor: writing moxie.sum: %w", err)
97 }
98
99 fmt.Printf("vendored %d modules into %s\n", len(mods), vendorDir)
100 return nil
101 }
102
103 type fileHash struct {
104 path string
105 hash string
106 }
107
108 func copyTree(src, dst string) ([]fileHash, error) {
109 var hashes []fileHash
110 err := filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
111 if err != nil {
112 return err
113 }
114 rel, _ := filepath.Rel(src, path)
115 target := filepath.Join(dst, rel)
116
117 if info.IsDir() {
118 base := filepath.Base(path)
119 if base == "vendor" || base == ".git" || base[0] == '.' {
120 return filepath.SkipDir
121 }
122 return os.MkdirAll(target, 0755)
123 }
124
125 base := filepath.Base(path)
126 if base[0] == '.' || base[0] == '_' {
127 return nil
128 }
129
130 data, err := os.ReadFile(path)
131 if err != nil {
132 return err
133 }
134 if err := os.WriteFile(target, data, 0644); err != nil {
135 return err
136 }
137
138 h := sha256.Sum256(data)
139 hashes = append(hashes, fileHash{
140 path: filepath.Join(filepath.Base(dst), rel),
141 hash: "sha256:" + hex.EncodeToString(h[:]),
142 })
143 return nil
144 })
145 return hashes, err
146 }
147