2 * This file is part of OpenTTD.
3 * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
4 * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
5 * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
8 /** @file settingsgen.cpp Tool to create computer-readable settings. */
10 #include "../stdafx.h"
11 #include "../string_func.h"
12 #include "../strings_type.h"
13 #include "../misc/getoptdata.h"
14 #include "../ini_type.h"
15 #include "../core/mem_func.hpp"
16 #include "../error_func.h"
20 #include "../safeguards.h"
23 * Report a fatal error.
24 * @param s Format string.
25 * @note Function does not return.
27 [[noreturn
]] void FatalErrorI(const std::string
&msg
)
29 fmt::print(stderr
, "settingsgen: FATAL: {}\n", msg
);
33 static const size_t OUTPUT_BLOCK_SIZE
= 16000; ///< Block size of the buffer in #OutputBuffer.
35 /** Output buffer for a block of data. */
38 /** Prepare buffer for use. */
45 * Add text to the output buffer.
46 * @param text Text to store.
47 * @param length Length of the text in bytes.
48 * @return Number of bytes actually stored.
50 size_t Add(const char *text
, size_t length
)
52 size_t store_size
= std::min(length
, OUTPUT_BLOCK_SIZE
- this->size
);
53 assert(store_size
<= OUTPUT_BLOCK_SIZE
);
54 MemCpyT(this->data
+ this->size
, text
, store_size
);
55 this->size
+= store_size
;
60 * Dump buffer to the output stream.
61 * @param out_fp Stream to write the \a data to.
63 void Write(FILE *out_fp
) const
65 if (fwrite(this->data
, 1, this->size
, out_fp
) != this->size
) {
66 FatalError("Cannot write output");
71 * Does the block have room for more data?
72 * @return \c true if room is available, else \c false.
76 return this->size
< OUTPUT_BLOCK_SIZE
;
79 size_t size
; ///< Number of bytes stored in \a data.
80 char data
[OUTPUT_BLOCK_SIZE
]; ///< Stored data.
83 /** Temporarily store output. */
91 /** Clear the temporary storage. */
94 this->output_buffer
.clear();
98 * Add text to the output storage.
99 * @param text Text to store.
100 * @param length Length of the text in bytes, \c 0 means 'length of the string'.
102 void Add(const char *text
, size_t length
= 0)
104 if (length
== 0) length
= strlen(text
);
106 if (length
> 0 && this->BufferHasRoom()) {
107 size_t stored_size
= this->output_buffer
[this->output_buffer
.size() - 1].Add(text
, length
);
108 length
-= stored_size
;
112 OutputBuffer
&block
= this->output_buffer
.emplace_back();
113 block
.Clear(); // Initialize the new block.
114 size_t stored_size
= block
.Add(text
, length
);
115 length
-= stored_size
;
121 * Write all stored output to the output stream.
122 * @param out_fp Stream to write the \a data to.
124 void Write(FILE *out_fp
) const
126 for (const OutputBuffer
&out_data
: output_buffer
) {
127 out_data
.Write(out_fp
);
133 * Does the buffer have room without adding a new #OutputBuffer block?
134 * @return \c true if room is available, else \c false.
136 bool BufferHasRoom() const
138 size_t num_blocks
= this->output_buffer
.size();
139 return num_blocks
> 0 && this->output_buffer
[num_blocks
- 1].HasRoom();
142 typedef std::vector
<OutputBuffer
> OutputBufferVector
; ///< Vector type for output buffers.
143 OutputBufferVector output_buffer
; ///< Vector of blocks containing the stored output.
147 /** Derived class for loading INI files without going through Fio stuff. */
148 struct SettingsIniFile
: IniLoadFile
{
150 * Construct a new ini loader.
151 * @param list_group_names A list with group names that should be loaded as lists instead of variables. @see IGT_LIST
152 * @param seq_group_names A list with group names that should be loaded as lists of names. @see IGT_SEQUENCE
154 SettingsIniFile(const IniGroupNameList
&list_group_names
= {}, const IniGroupNameList
&seq_group_names
= {}) :
155 IniLoadFile(list_group_names
, seq_group_names
)
159 std::optional
<FileHandle
> OpenFile(const std::string
&filename
, Subdirectory
, size_t *size
) override
161 /* Open the text file in binary mode to prevent end-of-line translations
162 * done by ftell() and friends, as defined by K&R. */
163 auto in
= FileHandle::Open(filename
, "rb");
164 if (!in
.has_value()) return in
;
166 fseek(*in
, 0L, SEEK_END
);
168 fseek(*in
, 0L, SEEK_SET
); // Seek back to the start of the file.
173 void ReportFileError(const char * const pre
, const char * const buffer
, const char * const post
) override
175 FatalError("{}{}{}", pre
, buffer
, post
);
179 OutputStore _stored_output
; ///< Temporary storage of the output, until all processing is done.
180 OutputStore _post_amble_output
; ///< Similar to _stored_output, but for the post amble.
182 static const char *PREAMBLE_GROUP_NAME
= "pre-amble"; ///< Name of the group containing the pre amble.
183 static const char *POSTAMBLE_GROUP_NAME
= "post-amble"; ///< Name of the group containing the post amble.
184 static const char *TEMPLATES_GROUP_NAME
= "templates"; ///< Name of the group containing the templates.
185 static const char *VALIDATION_GROUP_NAME
= "validation"; ///< Name of the group containing the validation statements.
186 static const char *DEFAULTS_GROUP_NAME
= "defaults"; ///< Name of the group containing default values for the template variables.
189 * Dump a #IGT_SEQUENCE group into #_stored_output.
190 * @param ifile Loaded INI data.
191 * @param group_name Name of the group to copy.
193 static void DumpGroup(const IniLoadFile
&ifile
, const char * const group_name
)
195 const IniGroup
*grp
= ifile
.GetGroup(group_name
);
196 if (grp
!= nullptr && grp
->type
== IGT_SEQUENCE
) {
197 for (const IniItem
&item
: grp
->items
) {
198 if (!item
.name
.empty()) {
199 _stored_output
.Add(item
.name
.c_str());
200 _stored_output
.Add("\n", 1);
207 * Find the value of a template variable.
208 * @param name Name of the item to find.
209 * @param grp Group currently being expanded (searched first).
210 * @param defaults Fallback group to search, \c nullptr skips the search.
211 * @return Text of the item if found, else \c nullptr.
213 static const char *FindItemValue(const char *name
, const IniGroup
*grp
, const IniGroup
*defaults
)
215 const IniItem
*item
= grp
->GetItem(name
);
216 if (item
== nullptr && defaults
!= nullptr) item
= defaults
->GetItem(name
);
217 if (item
== nullptr || !item
->value
.has_value()) return nullptr;
218 return item
->value
->c_str();
222 * Parse a single entry via a template and output this.
223 * @param item The template to use for the output.
224 * @param grp Group current being used for template rendering.
225 * @param default_grp Default values for items not set in @grp.
226 * @param output Output to use for result.
228 static void DumpLine(const IniItem
*item
, const IniGroup
*grp
, const IniGroup
*default_grp
, OutputStore
&output
)
230 static const int MAX_VAR_LENGTH
= 64;
232 /* Prefix with #if/#ifdef/#ifndef */
233 static const auto pp_lines
= {"if", "ifdef", "ifndef"};
235 for (const auto &name
: pp_lines
) {
236 const char *condition
= FindItemValue(name
, grp
, default_grp
);
237 if (condition
!= nullptr) {
241 output
.Add(condition
);
247 /* Output text of the template, except template variables of the form '$[_a-z0-9]+' which get replaced by their value. */
248 const char *txt
= item
->value
->c_str();
249 while (*txt
!= '\0') {
256 if (*txt
== '$') { // Literal $
263 char variable
[MAX_VAR_LENGTH
];
265 while (i
< MAX_VAR_LENGTH
- 1) {
266 if (!(txt
[i
] == '_' || (txt
[i
] >= 'a' && txt
[i
] <= 'z') || (txt
[i
] >= '0' && txt
[i
] <= '9'))) break;
267 variable
[i
] = txt
[i
];
274 /* Find the text to output. */
275 const char *valitem
= FindItemValue(variable
, grp
, default_grp
);
276 if (valitem
!= nullptr) output
.Add(valitem
);
281 output
.Add("\n", 1); // \n after the expanded template.
283 output
.Add("#endif\n");
289 * Output all non-special sections through the template / template variable expansion system.
290 * @param ifile Loaded INI data.
292 static void DumpSections(const IniLoadFile
&ifile
)
294 static const auto special_group_names
= {PREAMBLE_GROUP_NAME
, POSTAMBLE_GROUP_NAME
, DEFAULTS_GROUP_NAME
, TEMPLATES_GROUP_NAME
, VALIDATION_GROUP_NAME
};
296 const IniGroup
*default_grp
= ifile
.GetGroup(DEFAULTS_GROUP_NAME
);
297 const IniGroup
*templates_grp
= ifile
.GetGroup(TEMPLATES_GROUP_NAME
);
298 const IniGroup
*validation_grp
= ifile
.GetGroup(VALIDATION_GROUP_NAME
);
299 if (templates_grp
== nullptr) return;
301 /* Output every group, using its name as template name. */
302 for (const IniGroup
&grp
: ifile
.groups
) {
303 /* Exclude special group names. */
304 if (std::find(std::begin(special_group_names
), std::end(special_group_names
), grp
.name
) != std::end(special_group_names
)) continue;
306 const IniItem
*template_item
= templates_grp
->GetItem(grp
.name
); // Find template value.
307 if (template_item
== nullptr || !template_item
->value
.has_value()) {
308 FatalError("Cannot find template {}", grp
.name
);
310 DumpLine(template_item
, &grp
, default_grp
, _stored_output
);
312 if (validation_grp
!= nullptr) {
313 const IniItem
*validation_item
= validation_grp
->GetItem(grp
.name
); // Find template value.
314 if (validation_item
!= nullptr && validation_item
->value
.has_value()) {
315 DumpLine(validation_item
, &grp
, default_grp
, _post_amble_output
);
322 * Append a file to the output stream.
323 * @param fname Filename of file to append.
324 * @param out_fp Output stream to write to.
326 static void AppendFile(const char *fname
, FILE *out_fp
)
328 if (fname
== nullptr) return;
330 auto in_fp
= FileHandle::Open(fname
, "r");
331 if (!in_fp
.has_value()) {
332 FatalError("Cannot open file {} for copying", fname
);
338 length
= fread(buffer
, 1, lengthof(buffer
), *in_fp
);
339 if (fwrite(buffer
, 1, length
, out_fp
) != length
) {
340 FatalError("Cannot copy file");
342 } while (length
== lengthof(buffer
));
346 * Compare two files for identity.
347 * @param n1 First file.
348 * @param n2 Second file.
349 * @return True if both files are identical.
351 static bool CompareFiles(const char *n1
, const char *n2
)
353 auto f2
= FileHandle::Open(n2
, "rb");
354 if (!f2
.has_value()) return false;
356 auto f1
= FileHandle::Open(n1
, "rb");
357 if (!f1
.has_value()) {
358 FatalError("can't open {}", n1
);
365 l1
= fread(b1
, 1, sizeof(b1
), *f1
);
366 l2
= fread(b2
, 1, sizeof(b2
), *f2
);
368 if (l1
!= l2
|| memcmp(b1
, b2
, l1
) != 0) {
376 /** Options of settingsgen. */
377 static const OptionData _opts
[] = {
378 { .type
= ODF_NO_VALUE
, .id
= 'h', .shortname
= 'h', .longname
= "--help" },
379 { .type
= ODF_NO_VALUE
, .id
= 'h', .shortname
= '?' },
380 { .type
= ODF_HAS_VALUE
, .id
= 'o', .shortname
= 'o', .longname
= "--output" },
381 { .type
= ODF_HAS_VALUE
, .id
= 'b', .shortname
= 'b', .longname
= "--before" },
382 { .type
= ODF_HAS_VALUE
, .id
= 'a', .shortname
= 'a', .longname
= "--after" },
386 * Process a single INI file.
387 * The file should have a [templates] group, where each item is one template.
388 * Variables in a template have the form '\$[_a-z0-9]+' (a literal '$' followed
389 * by one or more '_', lowercase letters, or lowercase numbers).
391 * After loading, the [pre-amble] group is copied verbatim if it exists.
393 * For every group with a name that matches a template name the template is written.
394 * It starts with a optional \c \#if line if an 'if' item exists in the group. The item
395 * value is used as condition. Similarly, \c \#ifdef and \c \#ifndef lines are also written.
396 * Below the macro processor directives, the value of the template is written
397 * at a line with its variables replaced by item values of the group being written.
398 * If the group has no item for the variable, the [defaults] group is tried as fall back.
399 * Finally, \c \#endif lines are written to match the macro processor lines.
401 * Last but not least, the [post-amble] group is copied verbatim.
403 * @param fname Ini file to process. @return Exit status of the processing.
405 static void ProcessIniFile(const char *fname
)
407 static const IniLoadFile::IniGroupNameList seq_groups
= {PREAMBLE_GROUP_NAME
, POSTAMBLE_GROUP_NAME
};
409 SettingsIniFile ini
{{}, seq_groups
};
410 ini
.LoadFromDisk(fname
, NO_DIRECTORY
);
412 DumpGroup(ini
, PREAMBLE_GROUP_NAME
);
414 DumpGroup(ini
, POSTAMBLE_GROUP_NAME
);
418 * And the main program (what else?)
419 * @param argc Number of command-line arguments including the program name itself.
420 * @param argv Vector of the command-line arguments.
422 int CDECL
main(int argc
, char *argv
[])
424 const char *output_file
= nullptr;
425 const char *before_file
= nullptr;
426 const char *after_file
= nullptr;
428 GetOptData
mgo(std::span(argv
+ 1, argc
- 1), _opts
);
430 int i
= mgo
.GetOpt();
435 fmt::print("settingsgen\n"
436 "Usage: settingsgen [options] ini-file...\n"
438 " -h, -?, --help Print this help message and exit\n"
439 " -b FILE, --before FILE Copy FILE before all settings\n"
440 " -a FILE, --after FILE Copy FILE after all settings\n"
441 " -o FILE, --output FILE Write output to FILE\n");
445 output_file
= mgo
.opt
;
449 after_file
= mgo
.opt
;
453 before_file
= mgo
.opt
;
457 fmt::print(stderr
, "Invalid arguments\n");
462 _stored_output
.Clear();
463 _post_amble_output
.Clear();
465 for (auto &argument
: mgo
.arguments
) ProcessIniFile(argument
);
468 if (output_file
== nullptr) {
469 AppendFile(before_file
, stdout
);
470 _stored_output
.Write(stdout
);
471 _post_amble_output
.Write(stdout
);
472 AppendFile(after_file
, stdout
);
474 static const char * const tmp_output
= "tmp2.xxx";
476 auto fp
= FileHandle::Open(tmp_output
, "w");
477 if (!fp
.has_value()) {
478 FatalError("Cannot open file {}", tmp_output
);
480 AppendFile(before_file
, *fp
);
481 _stored_output
.Write(*fp
);
482 _post_amble_output
.Write(*fp
);
483 AppendFile(after_file
, *fp
);
486 std::error_code error_code
;
487 if (CompareFiles(tmp_output
, output_file
)) {
488 /* Files are equal. tmp2.xxx is not needed. */
489 std::filesystem::remove(tmp_output
, error_code
);
491 /* Rename tmp2.xxx to output file. */
492 std::filesystem::rename(tmp_output
, output_file
, error_code
);
493 if (error_code
) FatalError("rename({}, {}) failed: {}", tmp_output
, output_file
, error_code
.message());
500 * Simplified FileHandle::Open which ignores OTTD2FS. Required as settingsgen does not include all of the fileio system.
501 * @param filename UTF-8 encoded filename to open.
502 * @param mode Mode to open file.
503 * @return FileHandle, or std::nullopt on failure.
505 std::optional
<FileHandle
> FileHandle::Open(const std::string
&filename
, const std::string
&mode
)
507 auto f
= fopen(filename
.c_str(), mode
.c_str());
508 if (f
== nullptr) return std::nullopt
;
509 return FileHandle(f
);