module vcache import os import hash // Using a 2 level cache, ensures a more even distribution of cache entries, // so there will not be cramped folders that contain many thousands of them. // Most filesystems can not handle performantly such folders, and slow down. // The first level will contain a max of 256 folders, named from 00/ to ff/. // Each of them will contain many entries, but hopefully < 1000. // Note: using a hash here, makes the cache storage immune to special // characters in the keys, like quotes, spaces and so on. // Cleanup of the cache is simple: just delete the $VCACHE folder. // The cache tree will look like this: // │ $VCACHE // │ ├── README.md <-- a short description of the folder's purpose. // │ ├── 0f // │ │ ├── 0f004f983ab9c487b0d7c1a0a73840a5.txt // │ │ ├── 0f599edf5e16c2756fbcdd4c865087ac.output.description.txt <-- build details // │ │ └── 0f599edf5e16c2756fbcdd4c865087ac.vh // │ ├── 29 // │ │ ├── 294717dd02a1cca5f2a0393fca2c5c22.o // │ │ └── 294717dd02a1cca5f2a0393fca2c5c22.output.description.txt <-- build details // │ ├── 62 // │ │ └── 620d60d6b81fdcb3cab030a37fd86996.h // │ └── 76 // │ └── 7674f983ab9c487b0d7c1a0ad73840a5.c pub struct CacheManager { pub: basepath string original_vopts string pub mut: vopts string k2cpath map[string]string // key -> filesystem cache path for the object } fn remove_old_cache_folder() { // TODO: remove this after bootstrapping the new .cache location, i.e. after 2024-12-01 old_cache_folder := os.join_path(os.vmodules_dir(), 'cache') if os.exists(old_cache_folder) { old_readme_file := os.join_path(old_cache_folder, 'README.md') if os.file_size(old_readme_file) == 254 { os.rmdir_all(old_cache_folder) or {} dlog(@FN, 'old_cache_folder: ${old_cache_folder}') } } } pub fn new_cache_manager(opts []string) CacheManager { // use a path, that would not conflict with a user installable module. `import .cache` is not valid, => better than just `cache`: vcache_basepath := os.getenv_opt('VCACHE') or { os.join_path(os.vmodules_dir(), '.cache') } nlog(@FN, 'vcache_basepath: ${vcache_basepath}\n opts: ${opts}\n os.args: ${os.args.join(' ')}') dlog(@FN, 'vcache_basepath: ${vcache_basepath} | opts:\n ${opts}') if !os.is_dir(vcache_basepath) { remove_old_cache_folder() os.mkdir_all(vcache_basepath, mode: 0o700) or { panic(err) } // keep directory private dlog(@FN, 'created folder:\n ${vcache_basepath}') } readme_file := os.join_path(vcache_basepath, 'README.md') if !os.is_file(readme_file) { readme_content := 'This folder contains cached build artifacts from the V build system. |You can safely delete it, if it is getting too large. |It will be recreated the next time you compile something with V. |You can change its location with the VCACHE environment variable. '.strip_margin() os.write_file(readme_file, readme_content) or { panic(err) } dlog(@FN, 'created readme_file:\n ${readme_file}') } mut deduped_opts := map[string]bool{} for o in opts { deduped_opts[o] = true } deduped_opts_keys := deduped_opts.keys().filter(it != '' && !it.starts_with("['gcboehm', ")) // TODO: do not filter the gcboehm options here, instead just start `v build-module vlib/builtin` without the -d gcboehm etc. // Note: the current approach of filtering the gcboehm keys may interfere with (potential) other gc modes. original_vopts := deduped_opts_keys.join('|') return CacheManager{ basepath: vcache_basepath vopts: original_vopts original_vopts: original_vopts } } // set_temporary_options can be used to add temporary options to the hash salt // Note: these can be changed easily with another .set_temporary_options call // without affecting the .original_vopts pub fn (mut cm CacheManager) set_temporary_options(new_opts []string) { cm.vopts = cm.original_vopts + '#' + new_opts.join('|') cm.k2cpath = map[string]string{} dlog(@FN, 'cm.vopts:\n ${cm.vopts}') } pub fn (mut cm CacheManager) key2cpath(key string) string { mut cpath := cm.k2cpath[key] or { '' } if cpath == '' { hk := cm.vopts + key a := hash.sum64_string(hk, 5).hex_full() b := hash.sum64_string(hk, 7).hex_full() khash := a + b prefix := khash[0..2] cprefix_folder := os.join_path(cm.basepath, prefix) cpath = os.join_path(cprefix_folder, khash) if !os.is_dir(cprefix_folder) { os.mkdir_all(cprefix_folder) or { // The error here may be due to a race with another independent V process, that has already created the same folder. // If that is the case, so be it - just reuse the folder ¯\_(ツ)_/¯ ... if !os.is_dir(cprefix_folder) { panic(err) } } } dlog(@FN, 'new hk') dlog(@FN, ' key: ${key}') dlog(@FN, ' cpath: ${cpath}') dlog(@FN, ' cm.vopts:\n ${cm.vopts}') cm.k2cpath[key] = cpath } dlog(@FN, 'key: ${key:-30} => cpath: ${cpath}') return cpath } pub fn (mut cm CacheManager) postfix_with_key2cpath(postfix string, key string) string { prefix := cm.key2cpath(key) res := prefix + postfix return res } fn normalise_mod(mod string) string { return mod.replace('/', '.').replace('\\', '.').replace('vlib.', '').trim('.') } pub fn (mut cm CacheManager) mod_postfix_with_key2cpath(mod string, postfix string, key string) string { prefix := cm.key2cpath(key) res := '${prefix}.module.${normalise_mod(mod)}${postfix}' return res } pub fn (mut cm CacheManager) exists(postfix string, key string) !string { fpath := cm.postfix_with_key2cpath(postfix, key) dlog(@FN, 'postfix: ${postfix} | key: ${key} | fpath: ${fpath}') if !os.exists(fpath) { return error('does not exist yet') } return fpath } pub fn (mut cm CacheManager) mod_exists(mod string, postfix string, key string) !string { fpath := cm.mod_postfix_with_key2cpath(mod, postfix, key) dlog(@FN, 'mod: ${mod} | postfix: ${postfix} | key: ${key} | fpath: ${fpath}') if !os.exists(fpath) { return error('does not exist yet') } return fpath } // pub fn (mut cm CacheManager) save(postfix string, key string, content string) !string { fpath := cm.postfix_with_key2cpath(postfix, key) os.write_file(fpath, content)! dlog(@FN, 'postfix: ${postfix} | key: ${key} | fpath: ${fpath}') return fpath } pub fn (mut cm CacheManager) mod_save(mod string, postfix string, key string, content string) !string { fpath := cm.mod_postfix_with_key2cpath(mod, postfix, key) os.write_file(fpath, content)! dlog(@FN, 'mod: ${mod} | postfix: ${postfix} | key: ${key} | fpath: ${fpath}') return fpath } // pub fn (mut cm CacheManager) load(postfix string, key string) !string { fpath := cm.exists(postfix, key)! content := os.read_file(fpath)! dlog(@FN, 'postfix: ${postfix} | key: ${key} | fpath: ${fpath}') return content } pub fn (mut cm CacheManager) mod_load(mod string, postfix string, key string) !string { fpath := cm.mod_exists(mod, postfix, key)! content := os.read_file(fpath)! dlog(@FN, 'mod: ${mod} | postfix: ${postfix} | key: ${key} | fpath: ${fpath}') return content } @[if trace_usecache ?] pub fn dlog(fname string, s string) { xlog(fname, s) } @[if trace_usecache_n ?] fn nlog(fname string, s string) { xlog(fname, s) } fn xlog(fname string, s string) { pid := unsafe { mypid() } if fname[0] != `|` { eprintln('> VCache | pid: ${pid} | CacheManager.${fname} ${s}') } else { eprintln('> VCache | pid: ${pid} ${fname} ${s}') } } @[unsafe] fn mypid() int { mut static pid := 0 if pid == 0 { pid = os.getpid() } return pid }