The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
manager.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2008 - 2025
3 by Iris Morelle <shadowm2006@gmail.com>
4 Copyright (C) 2003 - 2008 by David White <dave@whitevine.net>
5 Part of the Battle for Wesnoth Project https://www.wesnoth.org/
6
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation; either version 2 of the License, or
10 (at your option) any later version.
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY.
13
14 See the COPYING file for more details.
15*/
16
17#include "addon/manager.hpp"
18
19#include "filesystem.hpp"
20#include "log.hpp"
23#include "utils/general.hpp"
24#include "game_version.hpp"
25#include "wml_exception.hpp"
26
27#include <boost/algorithm/string.hpp>
28
29static lg::log_domain log_config("config");
30#define ERR_CFG LOG_STREAM(err , log_config)
31#define LOG_CFG LOG_STREAM(info, log_config)
32#define WRN_CFG LOG_STREAM(warn, log_config)
33
34static lg::log_domain log_filesystem("filesystem");
35#define ERR_FS LOG_STREAM(err , log_filesystem)
36
37static lg::log_domain log_network("network");
38#define ERR_NET LOG_STREAM(err , log_network)
39#define LOG_NET LOG_STREAM(info, log_network)
40
41namespace {
42 std::string get_pbl_file_path(const std::string& addon_name)
43 {
44 const std::string& parentd = filesystem::get_addons_dir();
45 // Allow .pbl files directly in the addon dir
46 const std::string exterior = parentd + "/" + addon_name + ".pbl";
47 const std::string interior = parentd + "/" + addon_name + "/_server.pbl";
48 return filesystem::file_exists(exterior) ? exterior : interior;
49 }
50
51 inline std::string get_info_file_path(const std::string& addon_name)
52 {
53 return filesystem::get_addons_dir() + "/" + addon_name + "/_info.cfg";
54 }
55}
56
57bool have_addon_in_vcs_tree(const std::string& addon_name)
58{
59 static const std::string parentd = filesystem::get_addons_dir();
60 return
61 filesystem::file_exists(parentd+"/"+addon_name+"/.svn") ||
62 filesystem::file_exists(parentd+"/"+addon_name+"/.git") ||
63 filesystem::file_exists(parentd+"/"+addon_name+"/.hg");
64}
65
66bool have_addon_pbl_info(const std::string& addon_name)
67{
68 return filesystem::file_exists(get_pbl_file_path(addon_name));
69}
70
71config get_addon_pbl_info(const std::string& addon_name, bool do_validate)
72{
73 const std::string& pbl_path = get_pbl_file_path(addon_name);
74 try {
75 std::unique_ptr<schema_validation::schema_validator> validator;
76 if(do_validate) {
77 validator = std::make_unique<schema_validation::schema_validator>(filesystem::get_wml_location("schema/pbl.cfg").value());
78 validator->set_create_exceptions(true);
79 }
80 return io::read(*filesystem::istream_file(pbl_path), validator.get());
81 } catch(const config::error& e) {
82 throw invalid_pbl_exception(pbl_path, e.message);
83 } catch(wml_exception& e) {
84 auto msg = e.user_message;
85 e.user_message += " in " + addon_name;
86 boost::replace_all(e.dev_message, "<unknown>", filesystem::sanitize_path(pbl_path));
87 e.show();
88 throw invalid_pbl_exception(pbl_path, msg);
89 }
90}
91
92void set_addon_pbl_info(const std::string& addon_name, const config& cfg)
93{
94 io::write(*filesystem::ostream_file(get_pbl_file_path(addon_name)), cfg);
95}
96
97bool have_addon_install_info(const std::string& addon_name)
98{
99 return filesystem::file_exists(get_info_file_path(addon_name));
100}
101
102void get_addon_install_info(const std::string& addon_name, config& cfg)
103{
104 const std::string& info_path = get_info_file_path(addon_name);
106 try {
107 // The parser's read() API would normally do this at the start. This
108 // is a safeguard in case read() throws later
109 cfg.clear();
110 config envelope = io::read(*stream);
111 if(auto info = envelope.optional_child("info")) {
112 cfg = std::move(*info);
113 }
114 } catch(const config::error& e) {
115 ERR_CFG << "Failed to read add-on installation information for '"
116 << addon_name << "' from " << info_path << ":\n"
117 << e.message;
118 }
119}
120
121void write_addon_install_info(const std::string& addon_name, const config& cfg)
122{
123 LOG_CFG << "Writing version info for add-on '" << addon_name << "'";
124
125 const auto& info_path = get_info_file_path(addon_name);
126 auto out = filesystem::ostream_file(info_path);
127
128 *out << "#\n"
129 << "# File automatically generated by Wesnoth to keep track\n"
130 << "# of version information on installed add-ons. DO NOT EDIT!\n"
131 << "#\n";
132
133 config envelope;
134 envelope.add_child("info", cfg);
135 io::write(*out, envelope);
136}
137
138bool remove_local_addon(const std::string& addon)
139{
140 const std::string addon_dir = filesystem::get_addons_dir() + "/" + addon;
141
142 LOG_CFG << "removing local add-on: " << addon;
143
144 if(filesystem::file_exists(addon_dir) && !filesystem::delete_directory(addon_dir, true)) {
145 ERR_CFG << "Failed to delete directory/file: " << addon_dir;
146 ERR_CFG << "removal of add-on " << addon << " failed!";
147 return false;
148 }
149 return true;
150}
151
152namespace {
153
154enum ADDON_ENUM_CRITERIA
155{
156 ADDON_ANY,
157 ADDON_HAS_PBL,
158};
159
160std::vector<std::string> enumerate_addons_internal(ADDON_ENUM_CRITERIA filter)
161{
162 std::vector<std::string> res;
163 std::vector<std::string> addon_dirnames;
164
165 const auto& addons_root = filesystem::get_addons_dir();
166 filesystem::get_files_in_dir(addons_root, nullptr, &addon_dirnames);
167
168 for(const auto& addon_name : addon_dirnames) {
169 if(filesystem::file_exists(addons_root + "/" + addon_name + "/_main.cfg") &&
170 (filter != ADDON_HAS_PBL || have_addon_pbl_info(addon_name)))
171 {
172 res.emplace_back(addon_name);
173 }
174 }
175
176 return res;
177}
178
179}
180
181std::vector<std::string> available_addons()
182{
183 return enumerate_addons_internal(ADDON_HAS_PBL);
184}
185
186std::vector<std::string> installed_addons()
187{
188 return enumerate_addons_internal(ADDON_ANY);
189}
190
191std::map<std::string, std::string> installed_addons_and_versions()
192{
193 std::map<std::string, std::string> addons;
194
195 for(const std::string& addon_id : installed_addons()) {
196 if(have_addon_pbl_info(addon_id)) {
197 try {
198 // Just grabbing the version, so don't bother validating the pbl
199 addons[addon_id] = get_addon_pbl_info(addon_id, false)["version"].str();
200 } catch(const invalid_pbl_exception&) {
201 addons[addon_id] = "Invalid pbl file, version unknown";
202 }
203 } else if(filesystem::file_exists(get_info_file_path(addon_id))) {
204 config info_cfg;
205 get_addon_install_info(addon_id, info_cfg);
206 addons[addon_id] = !info_cfg.empty() ? info_cfg["version"].str() : "Unknown";
207 } else {
208 addons[addon_id] = "Unknown";
209 }
210 }
211 return addons;
212}
213
214bool is_addon_installed(const std::string& addon_name)
215{
216 const std::string namestem = filesystem::get_addons_dir() + "/" + addon_name;
217 return filesystem::file_exists(namestem + "/_main.cfg");
218}
219
220static inline bool IsCR(const char& c)
221{
222 return c == '\x0D';
223}
224
225static std::string strip_cr(std::string str, bool strip)
226{
227 if(!strip)
228 return str;
229 utils::erase_if(str, IsCR);
230 return str;
231}
232
233static filesystem::blacklist_pattern_list read_ignore_patterns(const std::string& addon_name)
234{
235 const std::string parentd = filesystem::get_addons_dir();
236 const std::string ign_file = parentd + "/" + addon_name + "/_server.ign";
237
239 LOG_CFG << "searching for .ign file for '" << addon_name << "'...";
240 if (!filesystem::file_exists(ign_file)) {
241 LOG_CFG << "no .ign file found for '" << addon_name << "'\n"
242 << "using default ignore patterns...";
244 }
245 LOG_CFG << "found .ign file: " << ign_file;
246 auto stream = filesystem::istream_file(ign_file);
247 std::string line;
248 while (std::getline(*stream, line)) {
249 boost::trim(line);
250 const std::size_t l = line.size();
251 // .gitignore & WML like comments
252 if (l == 0 || !line.compare(0,2,"# ")) continue;
253 if (line[l - 1] == '/') { // directory; we strip the last /
254 patterns.add_directory_pattern(line.substr(0, l - 1));
255 } else { // file
256 patterns.add_file_pattern(std::move(line));
257 }
258 }
259 return patterns;
260}
261
262static void archive_file(const std::string& path, const std::string& fname, config& cfg)
263{
264 cfg["name"] = fname;
265 cfg["contents"] = encode_binary(strip_cr(filesystem::read_file(path + '/' + fname), filesystem::is_cfg(fname)));
266}
267
268static void archive_dir(const std::string& path, const std::string& dirname, config& cfg, const filesystem::blacklist_pattern_list& ignore_patterns)
269{
270 cfg["name"] = dirname;
271 const std::string dir = path + '/' + dirname;
272
273 std::vector<std::string> files, dirs;
274 filesystem::get_files_in_dir(dir,&files,&dirs);
275 for(const std::string& name : files) {
276 bool valid = !filesystem::looks_like_pbl(name) && !ignore_patterns.match_file(name);
277 if (valid) {
278 archive_file(dir,name,cfg.add_child("file"));
279 }
280 }
281
282 for(const std::string& name : dirs) {
283 bool valid = !ignore_patterns.match_dir(name);
284 if (valid) {
285 archive_dir(dir,name,cfg.add_child("dir"),ignore_patterns);
286 }
287 }
288}
289
290void archive_addon(const std::string& addon_name, config& cfg)
291{
292 const std::string parentd = filesystem::get_addons_dir();
293
294 filesystem::blacklist_pattern_list ignore_patterns(read_ignore_patterns(addon_name));
295 archive_dir(parentd, addon_name, cfg.add_child("dir"), ignore_patterns);
296}
297
298static void unarchive_file(const std::string& path, const config& cfg)
299{
300 filesystem::write_file(path + '/' + cfg["name"].str(), unencode_binary(cfg["contents"]));
301}
302
303static void unarchive_dir(const std::string& path, const config& cfg, const std::function<void()>& file_callback = {})
304{
305 std::string dir;
306 if (cfg["name"].empty())
307 dir = path;
308 else
309 dir = path + '/' + cfg["name"].str();
310
312
313 for(const config &d : cfg.child_range("dir")) {
314 unarchive_dir(dir, d, file_callback);
315 }
316
317 for(const config &f : cfg.child_range("file")) {
318 unarchive_file(dir, f);
319 if(file_callback) {
320 file_callback();
321 }
322 }
323}
324
325static unsigned count_pack_files(const config& cfg)
326{
327 unsigned count = 0;
328
329 for(const config& d : cfg.child_range("dir")) {
330 count += count_pack_files(d);
331 }
332
333 return count + cfg.child_count("file");
334}
335
336void unarchive_addon(const config& cfg, std::function<void(unsigned)> progress_callback)
337{
338 const std::string parentd = filesystem::get_addons_dir();
339 unsigned file_count = progress_callback ? count_pack_files(cfg) : 0, done = 0;
340 auto file_callback = progress_callback
341 ? [&]() { progress_callback(++done * 100.0 / file_count); }
342 : std::function<void()>{};
343 unarchive_dir(parentd, cfg, file_callback);
344}
345
346static void purge_dir(const std::string& path, const config& removelist)
347{
348 std::string dir;
349 if(removelist["name"].empty())
350 dir = path;
351 else
352 dir = path + '/' + removelist["name"].str();
353
354 if(!filesystem::is_directory(dir)) {
355 return;
356 }
357
358 for(const config& d : removelist.child_range("dir")) {
359 purge_dir(dir, d);
360 }
361
362 for(const config& f : removelist.child_range("file")) {
363 filesystem::delete_file(dir + '/' + f["name"].str());
364 }
365
366 if(filesystem::dir_size(dir) < 1) {
368 }
369}
370
371void purge_addon(const config& removelist)
372{
373 const std::string parentd = filesystem::get_addons_dir();
374 purge_dir(parentd, removelist);
375}
376
377namespace {
378 std::map< std::string, version_info > version_info_cache;
379} // end unnamed namespace 5
380
382{
383 version_info_cache.clear();
384
385 LOG_CFG << "refreshing add-on versions cache";
386
387 const std::vector<std::string>& addons = installed_addons();
388 if(addons.empty()) {
389 return;
390 }
391
392 std::vector<std::string> addon_info_files(addons.size());
393
394 std::transform(addons.begin(), addons.end(),
395 addon_info_files.begin(), get_info_file_path);
396
397 for(std::size_t i = 0; i < addon_info_files.size(); ++i) {
398 assert(i < addons.size());
399
400 const std::string& addon = addons[i];
401 const std::string& info_file = addon_info_files[i];
402
403 if(filesystem::file_exists(info_file)) {
404 config info_cfg;
405 get_addon_install_info(addon, info_cfg);
406
407 if(info_cfg.empty()) {
408 continue;
409 }
410
411 const std::string& version = info_cfg["version"].str();
412 LOG_CFG << "cached add-on version: " << addon << " [" << version << "]";
413
414 version_info_cache[addon] = version;
415 } else if (!have_addon_pbl_info(addon) && !have_addon_in_vcs_tree(addon)) {
416 // Don't print the warning if the user is clearly the author
417 WRN_CFG << "add-on '" << addon << "' has no _info.cfg; cannot read version info";
418 }
419 }
420}
421
422version_info get_addon_version_info(const std::string& addon)
423{
424 static const version_info nil;
425 std::map< std::string, version_info >::iterator entry = version_info_cache.find(addon);
426 return entry != version_info_cache.end() ? entry->second : nil;
427}
static void archive_file(const std::string &path, const std::string &fname, config &cfg)
Definition manager.cpp:262
bool remove_local_addon(const std::string &addon)
Removes the specified add-on, deleting its full directory structure.
Definition manager.cpp:138
static bool IsCR(const char &c)
Definition manager.cpp:220
config get_addon_pbl_info(const std::string &addon_name, bool do_validate)
Gets the publish information for an add-on.
Definition manager.cpp:71
void unarchive_addon(const config &cfg, std::function< void(unsigned)> progress_callback)
Definition manager.cpp:336
bool have_addon_in_vcs_tree(const std::string &addon_name)
Returns whether the specified add-on appears to be managed by a VCS or not.
Definition manager.cpp:57
void set_addon_pbl_info(const std::string &addon_name, const config &cfg)
Definition manager.cpp:92
static lg::log_domain log_filesystem("filesystem")
#define ERR_CFG
Definition manager.cpp:30
std::map< std::string, std::string > installed_addons_and_versions()
Retrieves the ids and versions of all installed add-ons.
Definition manager.cpp:191
#define LOG_CFG
Definition manager.cpp:31
void purge_addon(const config &removelist)
Removes the listed files from the addon.
Definition manager.cpp:371
static void archive_dir(const std::string &path, const std::string &dirname, config &cfg, const filesystem::blacklist_pattern_list &ignore_patterns)
Definition manager.cpp:268
void archive_addon(const std::string &addon_name, config &cfg)
Archives an add-on into a config object for campaignd transactions.
Definition manager.cpp:290
static void purge_dir(const std::string &path, const config &removelist)
Definition manager.cpp:346
static void unarchive_dir(const std::string &path, const config &cfg, const std::function< void()> &file_callback={})
Definition manager.cpp:303
void get_addon_install_info(const std::string &addon_name, config &cfg)
Gets the installation info (_info.cfg) for an add-on.
Definition manager.cpp:102
void write_addon_install_info(const std::string &addon_name, const config &cfg)
Definition manager.cpp:121
static lg::log_domain log_network("network")
static unsigned count_pack_files(const config &cfg)
Definition manager.cpp:325
#define WRN_CFG
Definition manager.cpp:32
std::vector< std::string > available_addons()
Returns a list of local add-ons that can be published.
Definition manager.cpp:181
static std::string strip_cr(std::string str, bool strip)
Definition manager.cpp:225
bool have_addon_pbl_info(const std::string &addon_name)
Returns whether a .pbl file is present for the specified add-on or not.
Definition manager.cpp:66
bool have_addon_install_info(const std::string &addon_name)
Returns true if there is a local installation info (_info.cfg) file for the add-on.
Definition manager.cpp:97
static void unarchive_file(const std::string &path, const config &cfg)
Definition manager.cpp:298
std::vector< std::string > installed_addons()
Retrieves the names of all installed add-ons.
Definition manager.cpp:186
void refresh_addon_version_info_cache()
Refreshes the per-session cache of add-on's version information structs.
Definition manager.cpp:381
version_info get_addon_version_info(const std::string &addon)
Returns a particular installed add-on's version information.
Definition manager.cpp:422
bool is_addon_installed(const std::string &addon_name)
Check whether the specified add-on is currently installed.
Definition manager.cpp:214
static filesystem::blacklist_pattern_list read_ignore_patterns(const std::string &addon_name)
Definition manager.cpp:233
static lg::log_domain log_config("config")
A config object defines a single node in a WML file, with access to child nodes.
Definition config.hpp:157
config & add_child(std::string_view key)
Definition config.cpp:436
optional_config_impl< config > optional_child(std::string_view key, int n=0)
Equivalent to mandatory_child, but returns an empty optional if the nth child was not found.
Definition config.cpp:380
child_itors child_range(std::string_view key)
Definition config.cpp:268
std::size_t child_count(std::string_view key) const
Definition config.cpp:292
bool empty() const
Definition config.cpp:823
void clear()
Definition config.cpp:802
bool match_file(const std::string &name) const
void add_file_pattern(std::string pattern)
void add_directory_pattern(std::string pattern)
bool match_dir(const std::string &name) const
Represents version numbers.
const config * cfg
Declarations for File-IO.
Interfaces for manipulating version numbers of engine, add-ons, etc.
Standard logging facilities (interface).
int dir_size(const std::string &path)
Returns the sum of the sizes of the files contained in a directory.
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.
bool is_cfg(const std::string &filename)
Returns true if the file ends with the wmlfile extension.
bool delete_file(const std::string &filename)
bool is_directory(const std::string &fname)
Returns true if the given file is a directory.
utils::optional< std::string > get_wml_location(const std::string &path, const utils::optional< std::string > &current_dir=utils::nullopt)
Returns a translated path to the actual file or directory, if it exists.
std::string read_file(const std::string &fname)
Basic disk I/O - read file.
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::istream > scoped_istream
void write_file(const std::string &fname, const std::string &data, std::ios_base::openmode mode=std::ios_base::binary)
Throws io_exception if an error occurs.
bool looks_like_pbl(const std::string &file)
bool make_directory(const std::string &dirname)
bool delete_directory(const std::string &dirname, const bool keep_pbl=false)
const blacklist_pattern_list default_blacklist
std::string get_addons_dir()
std::string sanitize_path(const std::string &path)
Sanitizes a path to remove references to the user's name.
filesystem::scoped_istream istream_file(const std::string &fname, bool treat_failure_as_error=true)
std::string path
config read(std::istream &in, abstract_validator *validator)
Definition parser.cpp:610
void write(std::ostream &out, const configr_of &cfg, unsigned int level, bool strong_quotes)
Definition parser.cpp:748
void erase_if(Container &container, const Predicate &predicate)
Convenience wrapper for using std::remove_if on a container.
Definition general.hpp:108
One of the realizations of serialization/validator.hpp abstract validator.
std::unique_ptr< MIX_Audio, decltype(&MIX_DestroyAudio)> value
Definition sound.cpp:139
Exception thrown when the WML parser fails to read a .pbl file.
Definition manager.hpp:45
Helper class, don't construct this directly.
std::string unencode_binary(const std::string &str)
std::string encode_binary(const std::string &str)
Add a special kind of assert to validate whether the input from WML doesn't contain any problems that...
#define i
#define d
#define e
#define f
#define c