--!A cross-platform build utility based on Lua -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- Copyright (C) 2015-present, Xmake Open Source Community. -- -- @author ruki -- @file build_cache.lua -- -- imports import("core.base.bytes") import("core.base.hashset") import("core.base.global") import("core.cache.memcache") import("core.project.config") import("core.project.policy") import("core.project.project") import("utils.ci.is_running", {alias = "ci_is_running"}) import("private.service.client_config") import("private.service.remote_cache.client", {alias = "remote_cache_client"}) -- get memcache function _memcache() local cache = _g.memcache if not cache then cache = memcache.cache("build_cache") _g.memcache = cache end return cache end -- get exist info function _get_existinfo() local existinfo = _g.existinfo if existinfo == nil then existinfo = remote_cache_client.singleton():existinfo() _g.existinfo = existinfo end return existinfo end -- is enabled? function is_enabled(target) local key = tostring(target or "all") local result = _memcache():get2("enabled", key) if result == nil then -- target may be option instance if result == nil and target and target.policy then result = target:policy("build.ccache") end if result == nil and os.isfile(os.projectfile()) then local policy = project.policy("build.ccache") if policy ~= nil then result = policy end end -- disable ccache on ci if result == nil and ci_is_running() then local action_build_cache = _g._ACTION_BUILD_CACHE if action_build_cache == nil then action_build_cache = os.getenv("XMAKE_ACTION_BUILD_CACHE") _g._ACTION_BUILD_CACHE = action_build_cache or false end -- we cannot disable it if github-action-setup-xmake/build-cache is enabled if not action_build_cache then result = false end end -- disable ccache for msvc, because cl.exe preprocessor is too slower -- @see https://github.com/xmake-io/xmake/issues/3532 if result == nil and is_host("windows") and target and target.has_tool and target:has_tool("cxx", "cl") then result = false end if result == nil then result = config.get("ccache") end result = result or false _memcache():set2("enabled", key, result) end return result end -- is supported? function is_supported(sourcekind) local sourcekinds = _g.sourcekinds if sourcekinds == nil then sourcekinds = hashset.of("cc", "cxx", "mm", "mxx") _g.sourcekinds = sourcekinds end return sourcekinds:has(sourcekind) end -- get cache key function cachekey(program, cppinfo, envs) local cppfile = cppinfo.cppfile local cppflags = cppinfo.cppflags local items = {program} for _, cppflag in ipairs(cppflags) do if cppflag:startswith("-D") or cppflag:startswith("/D") then -- ignore `-Dxx` to improve the cache hit rate, as some source files may not use the defined macros. -- @see https://github.com/xmake-io/xmake/issues/2425 else table.insert(items, cppflag) end end table.insert(items, hash.xxhash128(cppfile)) if envs then local basename = path.basename(program) if basename == "cl" then for _, name in ipairs({"WindowsSDKVersion", "VCToolsVersion", "LIB"}) do local val = envs[name] if val then table.insert(items, val) end end end end return hash.strhash128(table.concat(items, "")) end -- get cache root directory function rootdir() local cachedir = _g.cachedir if not cachedir then cachedir = config.get("ccachedir") if not cachedir and project.policy("build.ccache.global_storage") then cachedir = path.join(global.directory(), ".build_cache") end if not cachedir then cachedir = path.join(config.builddir(), ".build_cache") end _g.cachedir = path.absolute(cachedir) end return cachedir end -- clean cached files function clean() os.rm(rootdir()) if remote_cache_client.is_connected() then client_config.load() remote_cache_client.singleton():clean() end end -- get hit rate function hitrate() local cache_hit_count = (_g.cache_hit_count or 0) local total_count = (_g.total_count or 0) if total_count > 0 then return math.floor(cache_hit_count * 100 / total_count) end return 0 end -- dump stats function dump_stats() local total_count = (_g.total_count or 0) local cache_hit_count = (_g.cache_hit_count or 0) local cache_miss_count = total_count - cache_hit_count local newfiles_count = (_g.newfiles_count or 0) local remote_hit_count = (_g.remote_hit_count or 0) local remote_newfiles_count = (_g.remote_newfiles_count or 0) local preprocess_error_count = (_g.preprocess_error_count or 0) local compile_fallback_count = (_g.compile_fallback_count or 0) local compile_total_time = (_g.compile_total_time or 0) local cache_hit_total_time = (_g.cache_hit_total_time or 0) local cache_miss_total_time = (_g.cache_miss_total_time or 0) vprint("") vprint("build cache stats:") vprint("cache directory: %s", rootdir()) vprint("cache hit rate: %d%%", hitrate()) vprint("cache hit: %d", cache_hit_count) vprint("cache hit total time: %0.3fs", cache_hit_total_time / 1000.0) vprint("cache miss: %d", cache_miss_count) vprint("cache miss total time: %0.3fs", cache_miss_total_time / 1000.0) vprint("new cached files: %d", newfiles_count) vprint("remote cache hit: %d", remote_hit_count) vprint("remote new cached files: %d", remote_newfiles_count) vprint("preprocess failed: %d", preprocess_error_count) vprint("compile fallback count: %d", compile_fallback_count) vprint("compile total time: %0.3fs", compile_total_time / 1000.0) vprint("") end -- get object file function get(cachekey) _g.total_count = (_g.total_count or 0) + 1 local objectfile_cached = path.join(rootdir(), cachekey:sub(1, 2):lower(), cachekey) local objectfile_infofile = objectfile_cached .. ".txt" if os.isfile(objectfile_cached) then _g.cache_hit_count = (_g.cache_hit_count or 0) + 1 return objectfile_cached, objectfile_infofile elseif remote_cache_client.is_connected() then return try { function () if not remote_cache_client.singleton():unreachable() then local exists, extrainfo = remote_cache_client.singleton():pull(cachekey, objectfile_cached) if exists and os.isfile(objectfile_cached) then _g.cache_hit_count = (_g.cache_hit_count or 0) + 1 _g.remote_hit_count = (_g.remote_hit_count or 0) + 1 if extrainfo then io.save(objectfile_infofile, extrainfo) end return objectfile_cached, objectfile_infofile end end end, catch { function (errors) if errors and policy.build_warnings() then cprint("${color.warning}fallback to the local cache, %s", tostring(errors)) end end } } end end -- put object file function put(cachekey, objectfile, extrainfo) local objectfile_cached = path.join(rootdir(), cachekey:sub(1, 2):lower(), cachekey) local objectfile_infofile = objectfile_cached .. ".txt" os.cp(objectfile, objectfile_cached) if extrainfo then io.save(objectfile_infofile, extrainfo) end _g.newfiles_count = (_g.newfiles_count or 0) + 1 if remote_cache_client.is_connected() then try { function () if not remote_cache_client.singleton():unreachable() then -- this file does not exist in remote server? push it to server -- -- we use the bloom filter to approximate whether it exists or not, -- which may result in a few less files being uploaded, but that's fine. local existinfo = _get_existinfo() if not existinfo or not existinfo:get(cachekey) then -- existinfo is just an initial snapshot, we need to go further and determine if the current file exists local cacheinfo = remote_cache_client.singleton():cacheinfo(cachekey) if not cacheinfo or not cacheinfo.exists then _g.remote_newfiles_count = (_g.remote_newfiles_count or 0) + 1 remote_cache_client.singleton():push(cachekey, objectfile, extrainfo) end end end end, catch { function (errors) if errors and policy.build_warnings() then cprint("${color.warning}fallback to the local cache, %s", tostring(errors)) end end } } end end -- build with cache function build(program, argv, opt) -- do preprocess opt = opt or {} local preprocess = assert(opt.preprocess, "preprocessor not found!") local compile = assert(opt.compile, "compiler not found!") local cppinfo = preprocess(program, argv, opt) if cppinfo then local cachekey = cachekey(program, cppinfo, opt.envs) local cache_hit_start_time = os.mclock() local objectfile_cached, objectfile_infofile = get(cachekey) if objectfile_cached then os.cp(objectfile_cached, cppinfo.objectfile) -- we need to update mtime for incremental compilation -- @see https://github.com/xmake-io/xmake/issues/2620 os.touch(cppinfo.objectfile, {mtime = os.time()}) -- we need to get outdata/errdata to show warnings, -- @see https://github.com/xmake-io/xmake/issues/2452 if objectfile_infofile and os.isfile(objectfile_infofile) then local extrainfo = io.load(objectfile_infofile) cppinfo.outdata = extrainfo.outdata cppinfo.errdata = extrainfo.errdata end _g.cache_hit_total_time = (_g.cache_hit_total_time or 0) + (os.mclock() - cache_hit_start_time) else -- do compile local preprocess_outdata = cppinfo.outdata local preprocess_errdata = cppinfo.errdata local compile_start_time = os.mclock() local compile_fallback = opt.compile_fallback if compile_fallback then local ok = try {function () compile(program, cppinfo, opt); return true end} if not ok then -- we fallback to compile original source file if compiling preprocessed file fails. -- https://github.com/xmake-io/xmake/issues/2467 local outdata, errdata = compile_fallback() cppinfo.outdata = outdata cppinfo.errdata = errdata _g.compile_fallback_count = (_g.compile_fallback_count or 0) + 1 end else compile(program, cppinfo, opt) end -- if no compiler output, we need use preprocessor output, because it maybe contains warning output if not cppinfo.outdata or #cppinfo.outdata == 0 then cppinfo.outdata = preprocess_outdata end if not cppinfo.errdata or #cppinfo.errdata == 0 then cppinfo.errdata = preprocess_errdata end _g.compile_total_time = (_g.compile_total_time or 0) + (os.mclock() - compile_start_time) if cachekey then local extrainfo if cppinfo.outdata and #cppinfo.outdata ~= 0 then extrainfo = extrainfo or {} extrainfo.outdata = cppinfo.outdata end if cppinfo.errdata and #cppinfo.errdata ~= 0 then extrainfo = extrainfo or {} extrainfo.errdata = cppinfo.errdata end local cache_miss_start_time = os.mclock() put(cachekey, cppinfo.objectfile, extrainfo) _g.cache_miss_total_time = (_g.cache_miss_total_time or 0) + (os.mclock() - cache_miss_start_time) end end os.tryrm(cppinfo.cppfile) else _g.preprocess_error_count = (_g.preprocess_error_count or 0) + 1 end return cppinfo end