The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
config_cache.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2008 - 2025
3 by Pauli Nieminen <paniemin@cc.hut.fi>
4 Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY.
12
13 See the COPYING file for more details.
14*/
15
16#define GETTEXT_DOMAIN "wesnoth-lib"
17
18#include "config_cache.hpp"
19#include "filesystem.hpp"
20#include "gettext.hpp"
21#include "log.hpp"
22#include "hash.hpp"
26#include "game_version.hpp"
27
28#include <boost/algorithm/string/replace.hpp>
29#include <boost/iostreams/filter/gzip.hpp>
30
31static lg::log_domain log_cache("cache");
32#define ERR_CACHE LOG_STREAM(err, log_cache)
33#define LOG_CACHE LOG_STREAM(info, log_cache)
34#define DBG_CACHE LOG_STREAM(debug, log_cache)
35
36namespace game_config
37{
38
39namespace
40{
41
42void add_builtin_defines(preproc_map& target)
43{
44#ifdef __APPLE__
45 target.try_emplace("APPLE");
46#endif
47
48#ifdef __ANDROID__
49 target.try_emplace("ANDROID");
50#endif
51
52#if defined(MOUSE_TOUCH_EMULATION) || defined(TARGET_OS_IPHONE)
53 target.try_emplace("IPHONEOS");
54#endif
55
56 target.try_emplace("WESNOTH_VERSION", game_config::wesnoth_version.str());
57
58 target.try_emplace("WESNOTH_VERSION_MAJOR", std::to_string(game_config::wesnoth_version.major_version()));
59 target.try_emplace("WESNOTH_VERSION_MINOR", std::to_string(game_config::wesnoth_version.minor_version()));
60 target.try_emplace("WESNOTH_VERSION_REVISION", std::to_string(game_config::wesnoth_version.revision_level()));
61 target.try_emplace("WESNOTH_VERSION_SPECIAL", game_config::wesnoth_version.special_version());
62}
63
64}
65
71
73 : force_valid_cache_(false)
74 , use_cache_(true)
75 , fake_invalid_cache_(false)
76 , defines_map_()
77 , cache_file_prefix_("cache-v" + boost::algorithm::replace_all_copy(game_config::revision, ":", "_") + "-")
78{
79 // To set-up initial defines map correctly
81}
82
84{
85 return defines_map_;
86}
87
89{
90 LOG_CACHE << "Clearing defines map!";
91 defines_map_.clear();
92
93 //
94 // Set-up default defines map.
95 //
96
97 add_builtin_defines(defines_map_);
98}
99
101{
102 return load_configs(file_path, validator);
103}
104
105void config_cache::write_file(const std::string& file_path, const config& cfg)
106{
109 writer.write(cfg);
110}
111
112void config_cache::write_file(const std::string& file_path, const preproc_map& defines)
113{
114 if(defines.empty()) {
115 if(filesystem::file_exists(file_path)) {
117 }
118 return;
119 }
120
123
124 // Write all defines to stream.
125 for(const preproc_map::value_type& define : defines) {
126 define.second.write(writer, define.first);
127 }
128}
129
130config config_cache::read_file(const std::string& file_path)
131{
132 return io::read_gz(*filesystem::istream_file(file_path));
133}
134
136{
138 // HACK: copy_map doesn't have built-in defines in some cases (issue #1924)
139 // or they may be out of date, so brute-force them in.
140 add_builtin_defines(res);
141 return res;
142}
143
148
149std::pair<config, preproc_map> config_cache::read_configs(const std::string& file_path, abstract_validator* validator)
150{
151 preproc_map copy_map = make_copy_map();
152 config cfg = read_configs(file_path, copy_map, validator);
153 return { std::move(cfg), std::move(copy_map) };
154}
155
156config config_cache::read_configs(const std::string& file_path, preproc_map& defines_map, abstract_validator* validator)
157{
158 return io::read(*preprocess_file(file_path, defines_map), validator);
159}
160
162{
163 static const std::string extension = ".gz";
164
165 std::stringstream defines_string;
166 defines_string << file_path;
167
168 bool is_valid = true;
169
170 //
171 // Only WESNOTH_VERSION and its variants are allowed to be non-empty.
172 //
173 static const std::set<std::string> allowed_non_empty_define_keys = {
174 "WESNOTH_VERSION",
175 "WESNOTH_VERSION_MAJOR",
176 "WESNOTH_VERSION_MINOR",
177 "WESNOTH_VERSION_REVISION",
178 "WESNOTH_VERSION_SPECIAL"
179 };
180
181 for(const auto& [key, define] : defines_map_) {
182 if((!define.value.empty() || !define.arguments.empty()) && allowed_non_empty_define_keys.find(key) == allowed_non_empty_define_keys.end()) {
183 is_valid = false;
184 ERR_CACHE << "Invalid preprocessor define: " << key;
185 break;
186 }
187
188 defines_string << " " << key;
189 }
190
191 // Do cache check only if define map is valid and
192 // caching is allowed.
193 const std::string& cache_path = filesystem::get_cache_dir();
194
195 if(is_valid && !cache_path.empty()) {
196 // Use a hash for a shorter display of the defines.
197 const std::string fname = cache_path + "/" +
199 utils::md5(defines_string.str()).hex_digest();
200 const std::string fname_checksum = fname + ".checksum" + extension;
201
203
205 try {
206 if(filesystem::file_exists(fname_checksum)) {
207 DBG_CACHE << "Reading checksum: " << fname_checksum;
208 dir_checksum = filesystem::file_tree_checksum{read_file(fname_checksum)};
209 }
210 } catch(const config::error&) {
211 ERR_CACHE << "cache checksum is corrupt";
212 } catch(const filesystem::io_exception&) {
213 ERR_CACHE << "error reading cache checksum";
214 } catch(const std::ios_base::failure&) {
215 ERR_CACHE << "error reading cache checksum";
216 }
217 }
218
220 LOG_CACHE << "skipping cache validation (forced)";
221 }
222
223 if(filesystem::file_exists(fname + extension) && (force_valid_cache_ || (dir_checksum == filesystem::data_tree_checksum()))) {
224 LOG_CACHE << "found valid cache at '" << fname << extension << "' with defines_map " << defines_string.str();
225 log_scope("read cache");
226
227 try {
228 config cfg = read_file(fname + extension);
229 const std::string define_file = fname + ".define" + extension;
230
231 if(filesystem::file_exists(define_file)) {
233 }
234
235 return cfg;
236 } catch(const config::error& e) {
237 ERR_CACHE << "cache " << fname << extension << " is corrupt. Loading from files: "<< e.message;
238 } catch(const filesystem::io_exception&) {
239 ERR_CACHE << "error reading cache " << fname << extension << ". Loading from files";
240 } catch (const boost::iostreams::gzip_error& e) {
241 //read_file -> ... -> read_gz can throw this exception.
242 ERR_CACHE << "cache " << fname << extension << " is corrupt. Error code: " << e.error();
243 }
244 }
245
246 LOG_CACHE << "no valid cache found. Writing cache to '" << fname << extension << " with defines_map "<< defines_string.str() << "'";
247
248 // Now we need queued defines so read them to memory
250
251 auto [cfg, defines] = read_configs(file_path, validator);
252 add_defines_map_diff(defines);
253
254 try {
255 write_file(fname + extension, cfg);
256 write_file(fname + ".define" + extension, defines);
257
258 config checksum_cfg;
259
261 write_file(fname_checksum, checksum_cfg);
262 } catch(const filesystem::io_exception&) {
263 ERR_CACHE << "could not write to cache '" << fname << "'";
264 }
265
266 return cfg;
267 }
268
269 LOG_CACHE << "Loading plain config instead of cache";
270
271 auto [cfg, defines] = read_configs(file_path, validator);
272 add_defines_map_diff(defines);
273 return cfg;
274}
275
276void config_cache::read_defines_file(const std::string& file_path)
277{
278 DBG_CACHE << "Reading cached defines from: " << file_path;
279 config file_cfg = read_file(file_path);
280
281 for(const auto [key, cfg] : file_cfg.all_children_view()) {
283 }
284}
285
287{
288 for(const std::string& p : config_cache_transaction::instance().get_define_files()) {
290 }
291}
292
294{
295 // Make sure that we have fake transaction if no real one is going on
296 fake_transaction fake;
297
298 if(use_cache_) {
299 return read_cache(config_path, validator);
300 } else {
301 auto [cfg, defines] = read_configs(config_path, validator);
302 add_defines_map_diff(defines);
303 return cfg;
304 }
305}
306
308{
309 fake_invalid_cache_ = force;
310}
311
313{
314 use_cache_ = use;
315}
316
318{
319 force_valid_cache_ = force;
320}
321
326
327void config_cache::add_define(const std::string& define)
328{
329 DBG_CACHE << "adding define: " << define;
330 defines_map_.try_emplace(define);
331
333 // we have to add this to active map too
335 }
336}
337
338void config_cache::remove_define(const std::string& define)
339{
340 DBG_CACHE << "removing define: " << define;
341 defines_map_.erase(define);
342
344 // we have to remove this from active map too
346 }
347}
348
350{
351 std::vector<std::string> files, dirs;
353
354 LOG_CACHE << "clean_cache(): " << files.size() << " files, "
355 << dirs.size() << " dirs to check";
356
357 const std::string& exclude_current = cache_file_prefix_ + "*";
358
359 bool status = true;
360
361 status &= delete_cache_files(files, exclude_current);
362 status &= delete_cache_files(dirs, exclude_current);
363
364 LOG_CACHE << "clean_cache(): done";
365
366 return status;
367}
368
370{
371 std::vector<std::string> files, dirs;
373
374 LOG_CACHE << "purge_cache(): deleting " << files.size() << " files, "
375 << dirs.size() << " dirs";
376
377 bool status = true;
378
379 status &= delete_cache_files(files);
380 status &= delete_cache_files(dirs);
381
382 LOG_CACHE << "purge_cache(): done";
383 return status;
384}
385
386bool config_cache::delete_cache_files(const std::vector<std::string>& paths,
387 const std::string& exclude_pattern)
388{
389 const bool delete_everything = exclude_pattern.empty();
390 bool status = true;
391
392 for(const std::string& file_path : paths)
393 {
394 if(!delete_everything) {
395 const std::string& fn = filesystem::base_name(file_path);
396
397 if(utils::wildcard_string_match(fn, exclude_pattern)) {
398 LOG_CACHE << "delete_cache_files(): skipping " << file_path
399 << " excluded by '" << exclude_pattern << "'";
400 continue;
401 }
402 }
403
404 LOG_CACHE << "delete_cache_files(): deleting " << file_path;
405 if(!filesystem::delete_directory(file_path)) {
406 ERR_CACHE << "delete_cache_files(): could not delete "
407 << file_path;
408 status = false;
409 }
410 }
411
412 return status;
413}
414
416 : define_filenames_()
417 , active_map_()
418{
419 assert(state_ == FREE);
420 state_ = NEW;
421 active_ = this;
422}
423
429
434
435const std::vector<std::string>& config_cache_transaction::get_define_files() const
436{
437 return define_filenames_;
438}
439
440void config_cache_transaction::add_define_file(const std::string& file)
441{
442 define_filenames_.push_back(file);
443}
444
446{
447 if(active_map_.empty()) {
448 active_map_.insert(defines_map.begin(), defines_map.end());
449 if(get_state() == NEW) {
450 state_ = ACTIVE;
451 }
452 }
453
454 return active_map_;
455}
456
458{
459 switch(get_state()) {
460 case ACTIVE: {
461 preproc_map temp;
462 std::set_difference(new_map.begin(),
463 new_map.end(),
464 active_map_.begin(),
465 active_map_.end(),
466 std::insert_iterator(temp, temp.begin()));
467
468 active_map_.insert(temp.begin(), temp.end());
469 std::swap(temp, new_map);
470 break;
471 }
472
473 case LOCKED:
474 new_map.clear();
475 break;
476
477 default:
478 break;
479 }
480}
481
482} // namespace game_config
Used in parsing config file.
Definition validator.hpp:38
Class for writing a config out to a file in pieces.
void write(const config &cfg, bool strong_quotes=false)
A config object defines a single node in a WML file, with access to child nodes.
Definition config.hpp:157
auto all_children_view() const
In-order iteration over all children.
Definition config.hpp:795
void add_defines_map_diff(preproc_map &defines_map)
std::vector< std::string > define_filenames_
static config_cache_transaction & instance()
const std::vector< std::string > & get_define_files() const
void lock()
Lock the transaction so no more macros are added.
preproc_map & get_active_map(const preproc_map &defines_map)
static config_cache_transaction * active_
void add_define_file(const std::string &file)
Singleton class to manage game config file caching.
void add_define(const std::string &define)
Add a entry to preproc defines map.
void recheck_filetree_checksum()
Force cache checksum validation.
config read_cache(const std::string &path, abstract_validator *validator=nullptr)
void write_file(const std::string &file, const config &cfg)
bool purge_cache()
Deletes all cache files.
void set_use_cache(bool use)
Enable/disable caching.
config load_configs(const std::string &path, abstract_validator *validator=nullptr)
static config_cache & instance()
Get reference to the singleton object.
preproc_map & make_copy_map()
const preproc_map & get_preproc_map() const
config read_file(const std::string &file)
void remove_define(const std::string &define)
Remove a entry to preproc defines map.
void clear_defines()
Clear stored defines map to default values.
bool clean_cache()
Deletes stale cache files not in use by the game.
config get_config(const std::string &path, abstract_validator *validator=nullptr)
Gets a config object from given path.
bool delete_cache_files(const std::vector< std::string > &paths, const std::string &exclude_pattern="")
std::pair< config, preproc_map > read_configs(const std::string &path, abstract_validator *validator)
void read_defines_file(const std::string &path)
void add_defines_map_diff(preproc_map &)
void set_force_valid_cache(bool force)
Enable/disable cache validation.
Holds a fake cache transaction if no real one is used.
virtual std::string hex_digest() const override
Definition hash.cpp:119
#define LOG_CACHE
static lg::log_domain log_cache("cache")
#define DBG_CACHE
#define ERR_CACHE
const config * cfg
Declarations for File-IO.
Interfaces for manipulating version numbers of engine, add-ons, etc.
Standard logging facilities (interface).
#define log_scope(description)
Definition log.hpp:275
Definition span.hpp:53
std::string get_cache_dir()
bool file_exists(const std::string &name)
Returns true if a file or directory with such name already exists.
void get_files_in_dir(const std::string &dir, std::vector< std::string > *files, std::vector< std::string > *dirs=nullptr, name_mode mode=name_mode::FILE_NAME_ONLY, filter_mode filter=filter_mode::NO_FILTER, reorder_mode reorder=reorder_mode::DONT_REORDER, file_tree_checksum *checksum=nullptr)
Get a list of all files and/or directories in a given directory.
std::string base_name(const std::string &file, const bool remove_extension=false)
Returns the base filename of a file, with directory name stripped.
const file_tree_checksum & data_tree_checksum(bool reset=false)
Get the time at which the data/ tree was last modified at.
filesystem::scoped_ostream ostream_file(const std::string &fname, std::ios_base::openmode mode=std::ios_base::binary, bool create_directory=true)
std::unique_ptr< std::ostream > scoped_ostream
bool delete_directory(const std::string &dirname, const bool keep_pbl=false)
filesystem::scoped_istream istream_file(const std::string &fname, bool treat_failure_as_error=true)
Game configuration data as global variables.
const version_info wesnoth_version(VERSION)
int cache_compression_level
const std::string revision
config read(std::istream &in, abstract_validator *validator)
Definition parser.cpp:610
config read_gz(std::istream &file, abstract_validator *validator)
Might throw a std::ios_base::failure especially a gzip_error.
Definition parser.cpp:666
bool wildcard_string_match(std::string_view str, std::string_view pat) noexcept
Performs pattern matching with wildcards.
filesystem::scoped_istream preprocess_file(const std::string &fname, preproc_map &defines)
Function to use the WML preprocessor on a file.
std::map< std::string, struct preproc_define > preproc_map
std::unordered_map< std::string, cache_item > cache
Definition sound.cpp:146
An exception object used when an IO error occurs.
static void insert(preproc_map &, const config &)
mock_party p
#define e