github.com/replit/upm@v0.0.0-20240423230255-9ce4fc3ea24c/internal/store/store.go (about) 1 // Package store handles reading and writing the .upm/store.json file. 2 // This file is used to cache several things for performance reasons, 3 // as described in the README. 4 package store 5 6 import ( 7 "context" 8 "encoding/json" 9 "os" 10 "path/filepath" 11 "strings" 12 13 "github.com/replit/upm/internal/api" 14 "github.com/replit/upm/internal/util" 15 "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" 16 ) 17 18 // st is a global object representing the store data. All functions in 19 // this file read and write to it. Only one store is supported. 20 var st *store 21 22 // currentVersion is the current store schema version. See the Version 23 // field in the store struct. 24 const currentVersion = 2 25 26 // getStoreLocation returns the file path of the JSON store. 27 func getStoreLocation() string { 28 loc, ok := os.LookupEnv("UPM_STORE") 29 if ok { 30 return loc 31 } else { 32 return ".upm/store.json" 33 } 34 } 35 36 // read reads the store from disk and writes it into the global 37 // variable st. If there is an error, it terminates the process. 38 func read() { 39 st = &store{} 40 defer func() { 41 st.Version = currentVersion 42 }() 43 44 filename := getStoreLocation() 45 bytes, err := os.ReadFile(filename) 46 47 if err != nil { 48 if os.IsNotExist(err) { 49 return 50 } 51 util.DieIO("%s: %s", filename, err) 52 } 53 54 if len(strings.TrimSpace(string(bytes))) > 0 { 55 if err = json.Unmarshal(bytes, st); err != nil { 56 util.DieProtocol("%s: %s", filename, err) 57 } 58 } 59 60 if st.Version != currentVersion { 61 st = &store{} 62 } 63 } 64 65 // readMaybe reads the store if it hasn't been read yet. 66 func readMaybe() { 67 if st == nil { 68 read() 69 } 70 } 71 72 // initLanguage creates an entry in the store for the given language, 73 // if necessary. (A language is just the name of a backend.) 74 func initLanguage(language, languageAlias string) { 75 if st.Languages == nil { 76 st.Languages = map[string]*storeLanguage{} 77 } 78 if st.Languages[language] == nil && (languageAlias == "" || st.Languages[languageAlias] == nil) { 79 st.Languages[language] = &storeLanguage{} 80 } 81 } 82 83 func getLanguageCache(language, languageAlias string) *storeLanguage { 84 if st.Languages == nil { 85 return nil 86 } 87 if st.Languages[language] != nil { 88 return st.Languages[language] 89 } 90 if languageAlias != "" && st.Languages[languageAlias] != nil { 91 return st.Languages[languageAlias] 92 } 93 return nil 94 } 95 96 // Write writes the current contents of the store from memory back to 97 // disk. If there is an error, it terminates the process. 98 func Write(ctx context.Context) { 99 //nolint:ineffassign,wastedassign,staticcheck 100 span, ctx := tracer.StartSpanFromContext(ctx, "store.Write") 101 defer span.Finish() 102 filename := getStoreLocation() 103 104 filename, err := filepath.Abs(filename) 105 if err != nil { 106 util.DieIO("%s: %s", filename, err) 107 } 108 109 directory, _ := filepath.Split(filename) 110 if err := os.MkdirAll(directory, 0o777); err != nil { 111 util.DieIO("%s: %s", directory, err) 112 } 113 114 content, err := json.Marshal(st) 115 if err != nil { 116 util.Panicf("Store.Write: %s", err) 117 } 118 content = append(content, '\n') 119 120 util.TryWriteAtomic(filename, content) 121 } 122 123 // HasSpecfileChanged returns false if the specfile exists and has not 124 // changed since the last time UpdateFileHashes was called, or if it 125 // doesn't exist and it didn't exist last time either. Otherwise, it 126 // returns true. 127 func HasSpecfileChanged(b api.LanguageBackend) bool { 128 readMaybe() 129 initLanguage(b.Name, b.Alias) 130 return hashFile(b.Specfile) != getLanguageCache(b.Name, b.Alias).SpecfileHash 131 } 132 133 // HasLockfileChanged returns false if the lockfile exists and has not 134 // changed since the last time UpdateFileHashes was called, or if it 135 // doesn't exist and it didn't exist last time either. Otherwise, it 136 // returns true. 137 func HasLockfileChanged(b api.LanguageBackend) bool { 138 readMaybe() 139 initLanguage(b.Name, b.Alias) 140 return hashFile(b.Lockfile) != getLanguageCache(b.Name, b.Alias).LockfileHash 141 } 142 143 // GuessWithCache returns b.Guess(), but re-uses a cached return value 144 // if possible. The cache is used if the matches of b.GuessRegexps 145 // against b.FilenamePatterns has not changed since the last time 146 // GuessWithCache was invoked. (This is only possible if the backend 147 // specifies b.GuessRegexps, which is not always the case. If the 148 // backend does specify b.GuessRegexps, then the return value of this 149 // function is cached.) If forceGuess is true, then write to but do 150 // not read from the cache. 151 func GuessWithCache(ctx context.Context, b api.LanguageBackend, forceGuess bool) map[string][]api.PkgName { 152 span, ctx := tracer.StartSpanFromContext(ctx, "GuessWithCache") 153 defer span.Finish() 154 readMaybe() 155 initLanguage(b.Name, b.Alias) 156 cache := getLanguageCache(b.Name, b.Alias) 157 old := cache.GuessedImportsHash 158 var new hash = "n/a" 159 // If no regexps, then we can't hash imports. Skip reading and 160 // writing the hash. 161 if len(b.GuessRegexps) > 0 { 162 new = hashImports(b) 163 cache.GuessedImportsHash = new 164 } 165 if forceGuess || new != old { 166 var pkgs map[string][]api.PkgName 167 success := true 168 if new != "" { 169 pkgs, success = b.Guess(ctx) 170 } else { 171 // If new is the empty string, that means 172 // (according to the interface of hashImports) 173 // that there were no regexp matches. In that 174 // case we shouldn't have any packages 175 // returned by the bare imports search. Might 176 // as well just skip the search, right? 177 pkgs = map[string][]api.PkgName{} 178 } 179 if !success { 180 // If bare imports search is not successful, 181 // e.g. due to syntax error, then don't update 182 // the hash. This will force the search to be 183 // redone next time. 184 cache.GuessedImportsHash = old 185 } 186 // Only cache result if we are going to use the cache, 187 // and skip caching if bare imports search was not 188 // successful (e.g. due to syntax error). Also, avoid 189 // updating the imports cache if search was not 190 // successful. You might think this condition isn't 191 // needed, but it actually is for the case where you 192 // run guess on import A, then on import B with syntax 193 // error, then on import A again. 194 if len(b.GuessRegexps) > 0 && success { 195 guessed := []string{} 196 for name := range pkgs { 197 guessed = append(guessed, string(name)) 198 } 199 cache.GuessedImports = guessed 200 } 201 return pkgs 202 } else { 203 pkgs := map[string][]api.PkgName{} 204 for _, name := range cache.GuessedImports { 205 pkgs[name] = []api.PkgName{api.PkgName(name)} 206 } 207 return pkgs 208 } 209 } 210 211 func ClearGuesses(ctx context.Context, b api.LanguageBackend) { 212 //nolint:ineffassign,wastedassign,staticcheck 213 span, ctx := tracer.StartSpanFromContext(ctx, "ClearGuesses") 214 defer span.Finish() 215 216 cache := getLanguageCache(b.Name, b.Alias) 217 218 cache.GuessedImports = nil 219 cache.GuessedImportsHash = "" 220 } 221 222 func Read(ctx context.Context, b api.LanguageBackend) { 223 //nolint:ineffassign,wastedassign,staticcheck 224 span, ctx := tracer.StartSpanFromContext(ctx, "store.Read") 225 defer span.Finish() 226 readMaybe() 227 initLanguage(b.Name, b.Alias) 228 } 229 230 // UpdateFileHashes caches the current states of the specfile and 231 // lockfile. Neither file need exist. 232 func UpdateFileHashes(ctx context.Context, b api.LanguageBackend) { 233 //nolint:ineffassign,wastedassign,staticcheck 234 span, ctx := tracer.StartSpanFromContext(ctx, "store.UpdateFileHashes") 235 defer span.Finish() 236 cache := getLanguageCache(b.Name, b.Alias) 237 cache.SpecfileHash = hashFile(b.Specfile) 238 cache.LockfileHash = hashFile(b.Lockfile) 239 }