From f065243cd218d7a351411a4d3c9a2c48dbedd680 Mon Sep 17 00:00:00 2001 From: Rangi <35663410+Rangi42@users.noreply.github.com> Date: Wed, 22 Oct 2025 17:05:59 -0400 Subject: [PATCH] Enable RGBGFX's CLI "at-files" for all programs (#1848) --- ARCHITECTURE.md | 3 + Makefile | 1 + include/asm/main.hpp | 7 +- include/cli.hpp | 20 + include/fix/main.hpp | 10 +- include/link/lexer.hpp | 2 +- include/link/main.hpp | 16 +- include/link/object.hpp | 7 +- man/rgbasm.1 | 17 + man/rgbfix.1 | 17 + man/rgbgfx.1 | 70 +-- man/rgblink.1 | 17 + src/CMakeLists.txt | 1 + src/asm/main.cpp | 701 +++++++++++----------- src/asm/output.cpp | 13 +- src/cli.cpp | 153 +++++ src/fix/fix.cpp | 10 +- src/fix/main.cpp | 389 +++++++------ src/gfx/main.cpp | 797 +++++++++++--------------- src/link/lexer.cpp | 4 +- src/link/main.cpp | 437 +++++++------- src/link/object.cpp | 7 +- src/link/output.cpp | 38 +- test/asm/test.sh | 8 +- test/fix/disable-warnings.flags | 2 +- test/fix/dollar-hex.flags | 2 +- test/fix/gameid-trunc.flags | 2 +- test/fix/header-edit.flags | 2 +- test/fix/overwrite.flags | 2 +- test/fix/test.sh | 12 +- test/fix/title-color-trunc-rev.flags | 4 +- test/fix/title-color-trunc.flags | 2 +- test/fix/title-compat-trunc-rev.flags | 4 +- test/fix/title-compat-trunc.flags | 2 +- test/fix/title-gameid-trunc-rev.flags | 4 +- test/fix/title-gameid-trunc.flags | 2 +- test/fix/title-pad.flags | 2 +- test/fix/title-pad.gb | Bin 512 -> 512 bytes test/fix/title.flags | 2 +- test/fix/title.gb | Bin 512 -> 512 bytes test/fix/unknown-mbc.flags | 1 - test/fix/verify-pad.flags | 6 +- test/fix/verify-trash.flags | 2 +- test/fix/verify.flags | 2 +- 44 files changed, 1466 insertions(+), 1334 deletions(-) create mode 100644 include/cli.hpp create mode 100644 src/cli.cpp diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 03442a22..1962408b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -120,6 +120,9 @@ These files in the `src/` directory are shared across multiple programs: often a - **`backtrace.cpp`:** Generic printing of location backtraces for RGBASM and RGBLINK. Allows configuring backtrace styles with a command-line flag (conventionally `-B/--backtrace`). Renders warnings in yellow, errors in red, and locations in cyan. +- **`cli.cpp`:** + A function for parsing command-line options, including RGBDS-specific "at-files" (a filename containing more options, prepended with an "`@`"). + This is the only file to use the extern/getopt.cpp variables and functions. - **`diagnostics.cpp`:** Generic warning/error diagnostic support for all programs. Allows command-line flags (conventionally `-W`) to have `no-`, `error=`, or `no-error=` prefixes, and `=` level suffixes; allows "meta" flags to affect groups of individual flags; and counts how many total errors there have been. Every program has its own `warning.cpp` file that uses this. - **`linkdefs.cpp`:** diff --git a/Makefile b/Makefile index 5493b84c..a0e7a07c 100644 --- a/Makefile +++ b/Makefile @@ -51,6 +51,7 @@ all: rgbasm rgblink rgbfix rgbgfx common_obj := \ src/extern/getopt.o \ + src/cli.o \ src/diagnostics.o \ src/style.o \ src/usage.o \ diff --git a/include/asm/main.hpp b/include/asm/main.hpp index a1dbbfe8..5d42405c 100644 --- a/include/asm/main.hpp +++ b/include/asm/main.hpp @@ -3,6 +3,7 @@ #ifndef RGBDS_ASM_MAIN_HPP #define RGBDS_ASM_MAIN_HPP +#include #include #include #include @@ -20,10 +21,10 @@ struct Options { char binDigits[2] = {'0', '1'}; // -b char gfxDigits[4] = {'0', '1', '2', '3'}; // -g FILE *dependFile = nullptr; // -M - std::string targetFileName; // -MQ, -MT + std::optional targetFileName{}; // -MQ, -MT MissingInclude missingIncludeState = INC_ERROR; // -MC, -MG bool generatePhonyDeps = false; // -MP - std::string objectFileName; // -o + std::optional objectFileName{}; // -o uint8_t padByte = 0; // -p uint64_t maxErrors = 0; // -X @@ -35,7 +36,7 @@ struct Options { void printDep(std::string const &depName) { if (dependFile) { - fprintf(dependFile, "%s: %s\n", targetFileName.c_str(), depName.c_str()); + fprintf(dependFile, "%s: %s\n", targetFileName->c_str(), depName.c_str()); } } }; diff --git a/include/cli.hpp b/include/cli.hpp new file mode 100644 index 00000000..f895a44e --- /dev/null +++ b/include/cli.hpp @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +#ifndef RGBDS_CLI_HPP +#define RGBDS_CLI_HPP + +#include +#include + +#include "extern/getopt.hpp" // option + +void cli_ParseArgs( + int argc, + char *argv[], + char const *shortOpts, + option const *longOpts, + void (*parseArg)(int, char *), + void (*fatal)(char const *, ...) +); + +#endif // RGBDS_CLI_HPP diff --git a/include/fix/main.hpp b/include/fix/main.hpp index 597f6063..2291b009 100644 --- a/include/fix/main.hpp +++ b/include/fix/main.hpp @@ -3,7 +3,9 @@ #ifndef RGBDS_FIX_MAIN_HPP #define RGBDS_FIX_MAIN_HPP +#include #include +#include #include "fix/mbc.hpp" // UNSPECIFIED, MbcType @@ -28,19 +30,19 @@ struct Options { uint16_t ramSize = UNSPECIFIED; // -r bool sgb = false; // -s - char const *gameID = nullptr; // -i + std::optional gameID; // -i uint8_t gameIDLen; - char const *newLicensee = nullptr; // -k + std::optional newLicensee; // -k uint8_t newLicenseeLen; - char const *logoFilename = nullptr; // -L + std::optional logoFilename; // -L uint8_t logo[48] = {}; MbcType cartridgeType = MBC_NONE; // -m uint8_t tpp1Rev[2]; - char const *title = nullptr; // -t + std::optional title; // -t uint8_t titleLen; }; diff --git a/include/link/lexer.hpp b/include/link/lexer.hpp index e1ae9b71..01821c86 100644 --- a/include/link/lexer.hpp +++ b/include/link/lexer.hpp @@ -10,6 +10,6 @@ void lexer_TraceCurrent(); void lexer_IncludeFile(std::string &&path); void lexer_IncLineNo(); -bool lexer_Init(char const *linkerScriptName); +bool lexer_Init(std::string const &linkerScriptName); #endif // RGBDS_LINK_LEXER_HPP diff --git a/include/link/main.hpp b/include/link/main.hpp index 2d11c2a5..2baafc3d 100644 --- a/include/link/main.hpp +++ b/include/link/main.hpp @@ -3,16 +3,18 @@ #ifndef RGBDS_LINK_MAIN_HPP #define RGBDS_LINK_MAIN_HPP +#include #include +#include struct Options { - bool isDmgMode; // -d - char const *mapFileName; // -m - bool noSymInMap; // -M - char const *symFileName; // -n - char const *overlayFileName; // -O - char const *outputFileName; // -o - uint8_t padValue; // -p + bool isDmgMode; // -d + std::optional mapFileName; // -m + bool noSymInMap; // -M + std::optional symFileName; // -n + std::optional overlayFileName; // -O + std::optional outputFileName; // -o + uint8_t padValue; // -p bool hasPadValue = false; // Setting these three to 0 disables the functionality uint16_t scrambleROMX; // -S diff --git a/include/link/object.hpp b/include/link/object.hpp index ed642cb6..a485b58d 100644 --- a/include/link/object.hpp +++ b/include/link/object.hpp @@ -3,10 +3,13 @@ #ifndef RGBDS_LINK_OBJECT_HPP #define RGBDS_LINK_OBJECT_HPP +#include +#include + // Read an object (.o) file, and add its info to the data structures. -void obj_ReadFile(char const *fileName, unsigned int fileID); +void obj_ReadFile(std::string const &filePath, size_t fileID); // Sets up object file reading -void obj_Setup(unsigned int nbFiles); +void obj_Setup(size_t nbFiles); #endif // RGBDS_LINK_OBJECT_HPP diff --git a/man/rgbasm.1 b/man/rgbasm.1 index 94bf33ff..19131e6a 100644 --- a/man/rgbasm.1 +++ b/man/rgbasm.1 @@ -351,6 +351,23 @@ disables this behavior. The default is 100 if .Nm is printing errors to a terminal, and 0 otherwise. +.It @ Ns Ar at_file +Read more options and arguments from a file, as if its contents were given on the command line. +Arguments are separated by whitespace or newlines. +Lines starting with a hash sign +.Pq Ql # +are considered comments and ignored. +.Pp +No shell processing is performed, such as wildcard or variable expansion. +There is no support for escaping or quoting whitespace to be included in arguments. +The standard +.Ql -- +to stop option processing also disables at-file processing. +Note that while +.Ql -- +can be used +.Em inside +an at-file, it only disables option processing within that at-file, and processing continues in the parent scope. .El .Sh DIAGNOSTICS Warnings are diagnostic messages that indicate possibly erroneous behavior that does not necessarily compromise the assembling process. diff --git a/man/rgbfix.1 b/man/rgbfix.1 index d72eb44a..0ff9584f 100644 --- a/man/rgbfix.1 +++ b/man/rgbfix.1 @@ -243,6 +243,23 @@ See the section for a list of warnings. .It Fl w Disable all warning output, even when turned into errors. +.It @ Ns Ar at_file +Read more options and arguments from a file, as if its contents were given on the command line. +Arguments are separated by whitespace or newlines. +Lines starting with a hash sign +.Pq Ql # +are considered comments and ignored. +.Pp +No shell processing is performed, such as wildcard or variable expansion. +There is no support for escaping or quoting whitespace to be included in arguments. +The standard +.Ql -- +to stop option processing also disables at-file processing. +Note that while +.Ql -- +can be used +.Em inside +an at-file, it only disables option processing within that at-file, and processing continues in the parent scope. .El .Sh DIAGNOSTICS Warnings are diagnostic messages that indicate possibly erroneous behavior that does not necessarily compromise the header-fixing process. diff --git a/man/rgbgfx.1 b/man/rgbgfx.1 index 6f602eb7..8db1fe58 100644 --- a/man/rgbgfx.1 +++ b/man/rgbgfx.1 @@ -487,69 +487,53 @@ Implies .It Fl Z , Fl \-columns Read squares from the PNG in column-major order (column by column), instead of the default row-major order (line by line). This primarily affects tile map and attribute map output, although it may also change generated tile data and palettes. +.It @ Ns Ar at_file +Read more options and arguments from a file, as if its contents were given on the command line. +Arguments are separated by whitespace or newlines. +Lines starting with a hash sign +.Pq Ql # +are considered comments and ignored. +.Pp +No shell processing is performed, such as wildcard or variable expansion. +There is no support for escaping or quoting whitespace to be included in arguments. +The standard +.Ql -- +to stop option processing also disables at-file processing. +Note that while +.Ql -- +can be used +.Em inside +an at-file, it only disables option processing within that at-file, and processing continues in the parent scope. +.Pp +See +.Sx At-files +below for an explanation of how this can be useful. .El .Ss At-files In a given project, many images are to be converted with different flags. -The traditional way of solving this problem has been to specify the different flags for each image in the Makefile / build script; this can be inconvenient, as it centralizes all those flags away from the images they concern. +The traditional way of solving this problem has been to specify the different flags for each image in the Makefile or build script; this can be inconvenient, as it centralizes all those flags away from the images they concern. .Pp -To avoid these drawbacks, -.Nm -supports +To avoid these drawbacks, you can use .Dq at-files : any command-line argument that begins with an at sign .Pq Ql @ -is interpreted as one. -The rest of the argument (without the @, that is) is interpreted as the path to a file, whose contents are interpreted as if given on the command line. +is interpreted as one, as documented above. At-files can be stored right next to the corresponding image, for example: .Pp .Dl $ rgbgfx -o image.2bpp -t image.tilemap @image.flags image.png .Pp -This will read additional flags from file +This will read additional flags from the file .Ql image.flags , -which could contains for example +which could contain, for example, .Ql -b 128 to specify a base offset for the image's tiles. The above command could be generated from the following .Xr make 1 -rule, for example: +rule: .Bd -literal -offset indent %.2bpp %.tilemap: %.flags %.png rgbgfx -o $*.2bpp -t $*.tilemap @$*.flags $*.png .Ed -.Pp -Since the contents of at-files are interpreted by -.Nm , -.Sy no shell processing is performed ; -for example, shell variables are not expanded -.Ql ( $PWD , -.Ql %WINDIR% , -etc.). -In at-files, lines that are empty or contain only whitespace are ignored; lines that begin with a hash sign -.Pq Ql # , -optionally preceded by whitespace, are considered comments and also ignored. -Each line can contain any number of arguments, which are separated by whitespace. -.Pq \&No quoting feature to prevent this is provided. -.Pp -Note that a leading -.Ql @ -has no special meaning on option arguments, and that the standard -.Ql -- -to stop option processing also disables at-file processing. -For example, the following command line reads command-line options from -.Ql tilesets/town.flags -then -.Ql tilesets.flags , -but processes -.Ql @tilesets/town.png -as the input image and outputs tile data to -.Ql @tilesets/town.2bpp : -.Pp -.Dl $ rgbgfx -o @tilesets/town.2bpp @tilesets/town.flags @tilesets.flags -- @tilesets/town.png -.Pp -At-files can also specify the input image directly, and call for more at-files, both using the regular syntax. -Note that while -.Ql -- -can be used in an at-file (with identical semantics), it is only effective inside of it\(emnormal option processing continues in the parent scope. .Sh PALETTE SPECIFICATION FORMATS The following formats are supported: .Bl -tag -width Ds diff --git a/man/rgblink.1 b/man/rgblink.1 index b690e7d3..6d4d5816 100644 --- a/man/rgblink.1 +++ b/man/rgblink.1 @@ -235,6 +235,23 @@ You can use this to make binary files that are not a ROM. When making a ROM, note that not using this is not a replacement for .Xr rgbfix 1 Ap s Fl p option! +.It @ Ns Ar at_file +Read more options and arguments from a file, as if its contents were given on the command line. +Arguments are separated by whitespace or newlines. +Lines starting with a hash sign +.Pq Ql # +are considered comments and ignored. +.Pp +No shell processing is performed, such as wildcard or variable expansion. +There is no support for escaping or quoting whitespace to be included in arguments. +The standard +.Ql -- +to stop option processing also disables at-file processing. +Note that while +.Ql -- +can be used +.Em inside +an at-file, it only disables option processing within that at-file, and processing continues in the parent scope. .El .Ss Scrambling algorithm The default section placement algorithm tries to place sections into as few banks as possible. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 614be3aa..70ebfe63 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,6 +4,7 @@ configure_file(version.cpp _version.cpp ESCAPE_QUOTES) set(common_src "extern/getopt.cpp" + "cli.cpp" "diagnostics.cpp" "style.cpp" "usage.cpp" diff --git a/src/asm/main.cpp b/src/asm/main.cpp index 4de58abc..e11d765b 100644 --- a/src/asm/main.cpp +++ b/src/asm/main.cpp @@ -19,8 +19,8 @@ #include #include "backtrace.hpp" +#include "cli.hpp" #include "diagnostics.hpp" -#include "extern/getopt.hpp" #include "helpers.hpp" #include "parser.hpp" // Generated from parser.y #include "platform.hpp" @@ -40,13 +40,17 @@ Options options; -static char const *dependFileName = nullptr; // -M -static std::unordered_map> stateFileSpecs; // -s +// Flags which must be processed after the option parsing finishes +static struct LocalOptions { + std::optional dependFileName; // -M + std::unordered_map> stateFileSpecs; // -s + std::optional inputFileName; // +} localOptions; // Short options static char const *optstring = "B:b:D:Eg:hI:M:o:P:p:Q:r:s:VvW:wX:"; -// Variables for the long-only options +// Long-only option variable static int longOpt; // `--color` and variants of `-M` // Equivalent long options @@ -105,134 +109,6 @@ static Usage usage = { }; // clang-format on -// LCOV_EXCL_START -static void verboseOutputConfig(int argc, char *argv[]) { - if (!checkVerbosity(VERB_CONFIG)) { - return; - } - - style_Set(stderr, STYLE_MAGENTA, false); - - fprintf(stderr, "rgbasm %s\n", get_package_version_string()); - - printVVVVVVerbosity(); - - fputs("Options:\n", stderr); - // -E/--export-all - if (options.exportAll) { - fputs("\tExport all labels by default\n", stderr); - } - // -b/--binary-digits - if (options.binDigits[0] != '0' || options.binDigits[1] != '1') { - fprintf( - stderr, "\tBinary digits: '%c', '%c'\n", options.binDigits[0], options.binDigits[1] - ); - } - // -g/--gfx-chars - if (options.gfxDigits[0] != '0' || options.gfxDigits[1] != '1' || options.gfxDigits[2] != '2' - || options.gfxDigits[3] != '3') { - fprintf( - stderr, - "\tGraphics characters: '%c', '%c', '%c', '%c'\n", - options.gfxDigits[0], - options.gfxDigits[1], - options.gfxDigits[2], - options.gfxDigits[3] - ); - } - // -Q/--q-precision - fprintf( - stderr, - "\tFixed-point precision: Q%d.%" PRIu8 "\n", - 32 - options.fixPrecision, - options.fixPrecision - ); - // -p/--pad-value - fprintf(stderr, "\tPad value: 0x%02" PRIx8 "\n", options.padByte); - // -r/--recursion-depth - fprintf(stderr, "\tMaximum recursion depth %zu\n", options.maxRecursionDepth); - // -X/--max-errors - if (options.maxErrors) { - fprintf(stderr, "\tMaximum %" PRIu64 " errors\n", options.maxErrors); - } - // -D/--define - static bool hasDefines = false; // `static` so `sym_ForEach` callback can see it - sym_ForEach([](Symbol &sym) { - if (!sym.isBuiltin && sym.type == SYM_EQUS) { - if (!hasDefines) { - fputs("\tDefinitions:\n", stderr); - hasDefines = true; - } - fprintf(stderr, "\t - def %s equs \"%s\"\n", sym.name.c_str(), sym.getEqus()->c_str()); - } - }); - // -s/--state - if (!stateFileSpecs.empty()) { - fputs("\tOutput state files:\n", stderr); - static char const *featureNames[NB_STATE_FEATURES] = { - "equ", - "var", - "equs", - "char", - "macro", - }; - for (auto const &[name, features] : stateFileSpecs) { - fprintf(stderr, "\t - %s: ", name == "-" ? "" : name.c_str()); - for (size_t i = 0; i < features.size(); ++i) { - if (i > 0) { - fputs(", ", stderr); - } - fputs(featureNames[features[i]], stderr); - } - putc('\n', stderr); - } - } - // asmfile - if (musl_optind < argc) { - fprintf(stderr, "\tInput asm file: %s", argv[musl_optind]); - if (musl_optind + 1 < argc) { - fprintf(stderr, " (and %d more)", argc - musl_optind - 1); - } - putc('\n', stderr); - } - // -o/--output - if (!options.objectFileName.empty()) { - fprintf(stderr, "\tOutput object file: %s\n", options.objectFileName.c_str()); - } - fstk_VerboseOutputConfig(); - if (dependFileName) { - fprintf( - stderr, - "\tOutput dependency file: %s\n", - strcmp(dependFileName, "-") ? dependFileName : "" - ); - // -MT or -MQ - if (!options.targetFileName.empty()) { - fprintf(stderr, "\tTarget file(s): %s\n", options.targetFileName.c_str()); - } - // -MG or -MC - switch (options.missingIncludeState) { - case INC_ERROR: - fputs("\tExit with an error on a missing dependency\n", stderr); - break; - case GEN_EXIT: - fputs("\tExit normally on a missing dependency\n", stderr); - break; - case GEN_CONTINUE: - fputs("\tContinue processing after a missing dependency\n", stderr); - break; - } - // -MP - if (options.generatePhonyDeps) { - fputs("\tGenerate phony dependencies\n", stderr); - } - } - fputs("Ready.\n", stderr); - - style_Reset(stderr); -} -// LCOV_EXCL_STOP - static std::string escapeMakeChars(std::string &str) { std::string escaped; size_t pos = 0; @@ -295,6 +171,332 @@ static std::vector parseStateFeatures(char *str) { return features; } +static void parseArg(int ch, char *arg) { + switch (ch) { + case 'B': + if (!trace_ParseTraceDepth(arg)) { + fatal("Invalid argument for option '-B'"); + } + break; + + case 'b': + if (strlen(arg) == 2) { + opt_B(arg); + } else { + fatal("Must specify exactly 2 characters for option '-b'"); + } + break; + + case 'D': { + char *equals = strchr(arg, '='); + if (equals) { + *equals = '\0'; + sym_AddString(arg, std::make_shared(equals + 1)); + } else { + sym_AddString(arg, std::make_shared("1")); + } + break; + } + + case 'E': + options.exportAll = true; + break; + + case 'g': + if (strlen(arg) == 4) { + opt_G(arg); + } else { + fatal("Must specify exactly 4 characters for option '-g'"); + } + break; + + // LCOV_EXCL_START + case 'h': + usage.printAndExit(0); + // LCOV_EXCL_STOP + + case 'I': + fstk_AddIncludePath(arg); + break; + + case 'M': + if (localOptions.dependFileName) { + warnx( + "Overriding dependency file \"%s\"", + *localOptions.dependFileName == "-" ? "" + : localOptions.dependFileName->c_str() + ); + } + localOptions.dependFileName = arg; + break; + + case 'o': + if (options.objectFileName) { + warnx("Overriding output file \"%s\"", options.objectFileName->c_str()); + } + options.objectFileName = arg; + break; + + case 'P': + fstk_AddPreIncludeFile(arg); + break; + + case 'p': + if (std::optional padByte = parseWholeNumber(arg); !padByte) { + fatal("Invalid argument for option '-p'"); + } else if (*padByte > 0xFF) { + fatal("Argument for option '-p' must be between 0 and 0xFF"); + } else { + opt_P(*padByte); + } + break; + + case 'Q': { + char const *precisionArg = arg; + if (precisionArg[0] == '.') { + ++precisionArg; + } + + if (std::optional precision = parseWholeNumber(precisionArg); !precision) { + fatal("Invalid argument for option '-Q'"); + } else if (*precision < 1 || *precision > 31) { + fatal("Argument for option '-Q' must be between 1 and 31"); + } else { + opt_Q(*precision); + } + break; + } + + case 'r': + if (std::optional maxDepth = parseWholeNumber(arg); !maxDepth) { + fatal("Invalid argument for option '-r'"); + } else if (errno == ERANGE) { + fatal("Argument for option '-r' is out of range"); + } else { + options.maxRecursionDepth = *maxDepth; + } + break; + + case 's': { + // Split ":" so `arg` is "" and `name` is "" + char *name = strchr(arg, ':'); + if (!name) { + fatal("Invalid argument for option '-s'"); + } + *name++ = '\0'; + + std::vector features = parseStateFeatures(arg); + + if (localOptions.stateFileSpecs.find(name) != localOptions.stateFileSpecs.end()) { + warnx("Overriding state file \"%s\"", name); + } + localOptions.stateFileSpecs.emplace(name, std::move(features)); + break; + } + + // LCOV_EXCL_START + case 'V': + printf("rgbasm %s\n", get_package_version_string()); + exit(0); + + case 'v': + incrementVerbosity(); + break; + // LCOV_EXCL_STOP + + case 'W': + opt_W(arg); + break; + + case 'w': + warnings.state.warningsEnabled = false; + break; + + case 'X': + if (std::optional maxErrors = parseWholeNumber(arg); !maxErrors) { + fatal("Invalid argument for option '-X'"); + } else if (*maxErrors > UINT64_MAX) { + fatal("Argument for option '-X' must be between 0 and %" PRIu64, UINT64_MAX); + } else { + options.maxErrors = *maxErrors; + } + break; + + case 0: // Long-only options + switch (longOpt) { + case 'c': + if (!style_Parse(arg)) { + fatal("Invalid argument for option '--color'"); + } + break; + + case 'C': + options.missingIncludeState = GEN_CONTINUE; + break; + + case 'G': + options.missingIncludeState = GEN_EXIT; + break; + + case 'P': + options.generatePhonyDeps = true; + break; + + case 'Q': + case 'T': { + std::string newTarget = arg; + if (longOpt == 'Q') { + newTarget = escapeMakeChars(newTarget); + } + if (options.targetFileName) { + *options.targetFileName += ' '; + *options.targetFileName += newTarget; + } else { + options.targetFileName = newTarget; + } + break; + } + } + break; + + case 1: // Positional argument + if (localOptions.inputFileName) { + usage.printAndExit("More than one input file specified"); + } + localOptions.inputFileName = arg; + break; + + // LCOV_EXCL_START + default: + usage.printAndExit(1); + // LCOV_EXCL_STOP + } +} + +// LCOV_EXCL_START +static void verboseOutputConfig() { + if (!checkVerbosity(VERB_CONFIG)) { + return; + } + + style_Set(stderr, STYLE_MAGENTA, false); + + fprintf(stderr, "rgbasm %s\n", get_package_version_string()); + + printVVVVVVerbosity(); + + fputs("Options:\n", stderr); + // -E/--export-all + if (options.exportAll) { + fputs("\tExport all labels by default\n", stderr); + } + // -b/--binary-digits + if (options.binDigits[0] != '0' || options.binDigits[1] != '1') { + fprintf( + stderr, "\tBinary digits: '%c', '%c'\n", options.binDigits[0], options.binDigits[1] + ); + } + // -g/--gfx-chars + if (options.gfxDigits[0] != '0' || options.gfxDigits[1] != '1' || options.gfxDigits[2] != '2' + || options.gfxDigits[3] != '3') { + fprintf( + stderr, + "\tGraphics characters: '%c', '%c', '%c', '%c'\n", + options.gfxDigits[0], + options.gfxDigits[1], + options.gfxDigits[2], + options.gfxDigits[3] + ); + } + // -Q/--q-precision + fprintf( + stderr, + "\tFixed-point precision: Q%d.%" PRIu8 "\n", + 32 - options.fixPrecision, + options.fixPrecision + ); + // -p/--pad-value + fprintf(stderr, "\tPad value: 0x%02" PRIx8 "\n", options.padByte); + // -r/--recursion-depth + fprintf(stderr, "\tMaximum recursion depth %zu\n", options.maxRecursionDepth); + // -X/--max-errors + if (options.maxErrors) { + fprintf(stderr, "\tMaximum %" PRIu64 " errors\n", options.maxErrors); + } + // -D/--define + static bool hasDefines = false; // `static` so `sym_ForEach` callback can see it + sym_ForEach([](Symbol &sym) { + if (!sym.isBuiltin && sym.type == SYM_EQUS) { + if (!hasDefines) { + fputs("\tDefinitions:\n", stderr); + hasDefines = true; + } + fprintf(stderr, "\t - def %s equs \"%s\"\n", sym.name.c_str(), sym.getEqus()->c_str()); + } + }); + // -s/--state + if (!localOptions.stateFileSpecs.empty()) { + fputs("\tOutput state files:\n", stderr); + static char const *featureNames[NB_STATE_FEATURES] = { + "equ", + "var", + "equs", + "char", + "macro", + }; + for (auto const &[name, features] : localOptions.stateFileSpecs) { + fprintf(stderr, "\t - %s: ", name == "-" ? "" : name.c_str()); + for (size_t i = 0; i < features.size(); ++i) { + if (i > 0) { + fputs(", ", stderr); + } + fputs(featureNames[features[i]], stderr); + } + putc('\n', stderr); + } + } + // asmfile + if (localOptions.inputFileName) { + fprintf( + stderr, + "\tInput asm file: %s\n", + *localOptions.inputFileName == "-" ? "" : localOptions.inputFileName->c_str() + ); + } + // -o/--output + if (options.objectFileName) { + fprintf(stderr, "\tOutput object file: %s\n", options.objectFileName->c_str()); + } + fstk_VerboseOutputConfig(); + if (localOptions.dependFileName) { + fprintf(stderr, "\tOutput dependency file: %s\n", localOptions.dependFileName->c_str()); + // -MT or -MQ + if (options.targetFileName) { + fprintf(stderr, "\tTarget file(s): %s\n", options.targetFileName->c_str()); + } + // -MG or -MC + switch (options.missingIncludeState) { + case INC_ERROR: + fputs("\tExit with an error on a missing dependency\n", stderr); + break; + case GEN_EXIT: + fputs("\tExit normally on a missing dependency\n", stderr); + break; + case GEN_CONTINUE: + fputs("\tContinue processing after a missing dependency\n", stderr); + break; + } + // -MP + if (options.generatePhonyDeps) { + fputs("\tGenerate phony dependencies\n", stderr); + } + } + fputs("Ready.\n", stderr); + + style_Reset(stderr); +} +// LCOV_EXCL_STOP + int main(int argc, char *argv[]) { // Support SOURCE_DATE_EPOCH for reproducible builds // https://reproducible-builds.org/docs/source-date-epoch/ @@ -311,239 +513,54 @@ int main(int argc, char *argv[]) { options.maxErrors = 100; // LCOV_EXCL_LINE } - // Parse CLI options - for (int ch; (ch = musl_getopt_long_only(argc, argv, optstring, longopts, nullptr)) != -1;) { - switch (ch) { - case 'B': - if (!trace_ParseTraceDepth(musl_optarg)) { - fatal("Invalid argument for option '-B'"); - } - break; + cli_ParseArgs(argc, argv, optstring, longopts, parseArg, fatal); - case 'b': - if (strlen(musl_optarg) == 2) { - opt_B(musl_optarg); - } else { - fatal("Must specify exactly 2 characters for option '-b'"); - } - break; - - case 'D': { - char *equals = strchr(musl_optarg, '='); - if (equals) { - *equals = '\0'; - sym_AddString(musl_optarg, std::make_shared(equals + 1)); - } else { - sym_AddString(musl_optarg, std::make_shared("1")); - } - break; - } - - case 'E': - options.exportAll = true; - break; - - case 'g': - if (strlen(musl_optarg) == 4) { - opt_G(musl_optarg); - } else { - fatal("Must specify exactly 4 characters for option '-g'"); - } - break; - - // LCOV_EXCL_START - case 'h': - usage.printAndExit(0); - // LCOV_EXCL_STOP - - case 'I': - fstk_AddIncludePath(musl_optarg); - break; - - case 'M': - if (dependFileName) { - warnx( - "Overriding dependency file \"%s\"", - strcmp(dependFileName, "-") ? dependFileName : "" - ); - } - dependFileName = musl_optarg; - break; - - case 'o': - if (!options.objectFileName.empty()) { - warnx("Overriding output file \"%s\"", options.objectFileName.c_str()); - } - options.objectFileName = musl_optarg; - break; - - case 'P': - fstk_AddPreIncludeFile(musl_optarg); - break; - - case 'p': - if (std::optional padByte = parseWholeNumber(musl_optarg); !padByte) { - fatal("Invalid argument for option '-p'"); - } else if (*padByte > 0xFF) { - fatal("Argument for option '-p' must be between 0 and 0xFF"); - } else { - opt_P(*padByte); - } - break; - - case 'Q': { - char const *precisionArg = musl_optarg; - if (precisionArg[0] == '.') { - ++precisionArg; - } - - if (std::optional precision = parseWholeNumber(precisionArg); !precision) { - fatal("Invalid argument for option '-Q'"); - } else if (*precision < 1 || *precision > 31) { - fatal("Argument for option '-Q' must be between 1 and 31"); - } else { - opt_Q(*precision); - } - break; - } - - case 'r': - if (std::optional maxDepth = parseWholeNumber(musl_optarg); !maxDepth) { - fatal("Invalid argument for option '-r'"); - } else if (errno == ERANGE) { - fatal("Argument for option '-r' is out of range"); - } else { - options.maxRecursionDepth = *maxDepth; - } - break; - - case 's': { - // Split ":" so `musl_optarg` is "" and `name` is "" - char *name = strchr(musl_optarg, ':'); - if (!name) { - fatal("Invalid argument for option '-s'"); - } - *name++ = '\0'; - - std::vector features = parseStateFeatures(musl_optarg); - - if (stateFileSpecs.find(name) != stateFileSpecs.end()) { - warnx("Overriding state file \"%s\"", name); - } - stateFileSpecs.emplace(name, std::move(features)); - break; - } - - // LCOV_EXCL_START - case 'V': - printf("rgbasm %s\n", get_package_version_string()); - exit(0); - - case 'v': - incrementVerbosity(); - break; - // LCOV_EXCL_STOP - - case 'W': - opt_W(musl_optarg); - break; - - case 'w': - warnings.state.warningsEnabled = false; - break; - - case 'X': - if (std::optional maxErrors = parseWholeNumber(musl_optarg); !maxErrors) { - fatal("Invalid argument for option '-X'"); - } else if (*maxErrors > UINT64_MAX) { - fatal("Argument for option '-X' must be between 0 and %" PRIu64, UINT64_MAX); - } else { - options.maxErrors = *maxErrors; - } - break; - - case 0: // Long-only options - switch (longOpt) { - case 'c': - if (!style_Parse(musl_optarg)) { - fatal("Invalid argument for option '--color'"); - } - break; - - case 'C': - options.missingIncludeState = GEN_CONTINUE; - break; - - case 'G': - options.missingIncludeState = GEN_EXIT; - break; - - case 'P': - options.generatePhonyDeps = true; - break; - - case 'Q': - case 'T': { - std::string newTarget = musl_optarg; - if (longOpt == 'Q') { - newTarget = escapeMakeChars(newTarget); - } - if (!options.targetFileName.empty()) { - options.targetFileName += ' '; - } - options.targetFileName += newTarget; - break; - } - } - break; - - // LCOV_EXCL_START - default: - usage.printAndExit(1); - // LCOV_EXCL_STOP - } - } - - if (options.targetFileName.empty() && !options.objectFileName.empty()) { + if (!options.targetFileName && options.objectFileName) { options.targetFileName = options.objectFileName; } - verboseOutputConfig(argc, argv); + verboseOutputConfig(); - if (argc == musl_optind) { + if (!localOptions.inputFileName) { usage.printAndExit("No input file specified (pass \"-\" to read from standard input)"); - } else if (argc != musl_optind + 1) { - usage.printAndExit("More than one input file specified"); } - std::string mainFileName = argv[musl_optind]; + // LCOV_EXCL_START + verbosePrint( + VERB_NOTICE, + "Assembling \"%s\"\n", + *localOptions.inputFileName == "-" ? "" : localOptions.inputFileName->c_str() + ); + // LCOV_EXCL_STOP - verbosePrint(VERB_NOTICE, "Assembling \"%s\"\n", mainFileName.c_str()); // LCOV_EXCL_LINE - - if (dependFileName) { - if (options.targetFileName.empty()) { + if (localOptions.dependFileName) { + if (!options.targetFileName) { fatal("Dependency files can only be created if a target file is specified with either " "'-o', '-MQ' or '-MT'"); } - if (strcmp("-", dependFileName)) { - options.dependFile = fopen(dependFileName, "w"); + if (*localOptions.dependFileName == "-") { + options.dependFile = stdout; + } else { + options.dependFile = fopen(localOptions.dependFileName->c_str(), "w"); if (options.dependFile == nullptr) { // LCOV_EXCL_START - fatal("Failed to open dependency file \"%s\": %s", dependFileName, strerror(errno)); + fatal( + "Failed to open dependency file \"%s\": %s", + localOptions.dependFileName->c_str(), + strerror(errno) + ); // LCOV_EXCL_STOP } - } else { - options.dependFile = stdout; } } - options.printDep(mainFileName); + options.printDep(*localOptions.inputFileName); charmap_New(DEFAULT_CHARMAP_NAME, nullptr); // Init lexer and file stack, providing file info - fstk_Init(mainFileName); + fstk_Init(*localOptions.inputFileName); // Perform parse (`yy::parser` is auto-generated from `parser.y`) if (yy::parser parser; parser.parse() != 0) { @@ -569,7 +586,7 @@ int main(int argc, char *argv[]) { out_WriteObject(); - for (auto const &[name, features] : stateFileSpecs) { + for (auto const &[name, features] : localOptions.stateFileSpecs) { out_WriteState(name, features); } diff --git a/src/asm/output.cpp b/src/asm/output.cpp index fbbbfd85..f49fc0c1 100644 --- a/src/asm/output.cpp +++ b/src/asm/output.cpp @@ -191,23 +191,22 @@ static void writeFileStackNode(FileStackNode const &node, FILE *file) { } void out_WriteObject() { - if (options.objectFileName.empty()) { + if (!options.objectFileName) { return; } static FILE *file; // `static` so `sect_ForEach` callback can see it - if (options.objectFileName != "-") { - file = fopen(options.objectFileName.c_str(), "wb"); + char const *objectFileName = options.objectFileName->c_str(); + if (*options.objectFileName != "-") { + file = fopen(objectFileName, "wb"); } else { - options.objectFileName = ""; + objectFileName = ""; (void)setmode(STDOUT_FILENO, O_BINARY); file = stdout; } if (!file) { // LCOV_EXCL_START - fatal( - "Failed to open object file \"%s\": %s", options.objectFileName.c_str(), strerror(errno) - ); + fatal("Failed to open object file \"%s\": %s", objectFileName, strerror(errno)); // LCOV_EXCL_STOP } Defer closeFile{[&] { fclose(file); }}; diff --git a/src/cli.cpp b/src/cli.cpp new file mode 100644 index 00000000..2c8197e0 --- /dev/null +++ b/src/cli.cpp @@ -0,0 +1,153 @@ +#include "cli.hpp" + +#include +#include +#include +#include +#include + +#include "extern/getopt.hpp" +#include "util.hpp" // isBlankSpace + +using namespace std::literals; + +// Turn an at-file's contents into an argv that `getopt` can handle, appending them to `argPool`. +static std::vector readAtFile( + std::string const &path, std::vector &argPool, void (*fatal)(char const *, ...) +) { + std::vector argvOfs; + + std::filebuf file; + if (!file.open(path, std::ios_base::in)) { + std::string msg = "Error reading at-file \""s + path + "\": " + strerror(errno); + fatal(msg.c_str()); + return argvOfs; // Since we can't mark the `fatal` function pointer as [[noreturn]] + } + + for (;;) { + int c = file.sbumpc(); + + // First, discard any leading blank space + while (isBlankSpace(c)) { + c = file.sbumpc(); + } + + // If it's a comment, discard everything until EOL + if (c == '#') { + c = file.sbumpc(); + while (c != EOF && !isNewline(c)) { + c = file.sbumpc(); + } + } + + if (c == EOF) { + return argvOfs; + } else if (isNewline(c)) { + continue; // Start processing the next line + } + + // Alright, now we can parse the line + do { + argvOfs.push_back(argPool.size()); + + // Read one argument (until the next whitespace char). + // We know there is one because we already have its first character in `c`. + for (; c != EOF && !isWhitespace(c); c = file.sbumpc()) { + argPool.push_back(c); + } + argPool.push_back('\0'); + + // Discard blank space until the next argument (candidate) + while (isBlankSpace(c)) { + c = file.sbumpc(); + } + } while (c != EOF && !isNewline(c)); // End if we reached EOL + } +} + +void cli_ParseArgs( + int argc, + char *argv[], + char const *shortOpts, + option const *longOpts, + void (*parseArg)(int, char *), + void (*fatal)(char const *, ...) +) { + struct AtFileStackEntry { + int parentInd; // Saved offset into parent argv + std::vector argv; // This context's arg pointer vec + + AtFileStackEntry(int parentInd_, std::vector argv_) + : parentInd(parentInd_), argv(argv_) {} + }; + std::vector atFileStack; + + int curArgc = argc; + char **curArgv = argv; + std::string optString = "-"s + shortOpts; // Request position arguments with a leading '-' + std::vector> argPools; + + for (;;) { + char *atFileName = nullptr; + for (int ch; + (ch = musl_getopt_long_only(curArgc, curArgv, optString.c_str(), longOpts, nullptr)) + != -1;) { + if (ch == 1 && musl_optarg[0] == '@') { + atFileName = &musl_optarg[1]; + break; + } else { + parseArg(ch, musl_optarg); + } + } + + if (atFileName) { + // We need to allocate a new arg pool for each at-file, so as not to invalidate pointers + // previous at-files may have generated to their own arg pools. + // But for the same reason, the arg pool must also outlive the at-file's stack entry! + std::vector &argPool = argPools.emplace_back(); + + // Copy `argv[0]` for error reporting, and because option parsing skips it + AtFileStackEntry &stackEntry = + atFileStack.emplace_back(musl_optind, std::vector{atFileName}); + + // It would be nice to compute the char pointers on the fly, but reallocs don't allow + // that; so we must compute the offsets after the pool is fixed + std::vector offsets = readAtFile(&musl_optarg[1], argPool, fatal); + stackEntry.argv.reserve(offsets.size() + 2); // Avoid a bunch of reallocs + for (size_t ofs : offsets) { + stackEntry.argv.push_back(&argPool.data()[ofs]); + } + stackEntry.argv.push_back(nullptr); // Don't forget the arg vector terminator! + + curArgc = stackEntry.argv.size() - 1; + curArgv = stackEntry.argv.data(); + musl_optind = 1; // Don't use 0 because we're not scanning a different argv per se + } else { + if (musl_optind != curArgc) { + // This happens if `--` is passed, process the remaining arg(s) as positional + assume(musl_optind < curArgc); + for (int i = musl_optind; i < curArgc; ++i) { + parseArg(1, argv[i]); // Positional argument + } + } + + // Pop off the top stack entry, or end parsing if none + if (atFileStack.empty()) { + break; + } + + // OK to restore `optind` directly, because `optpos` must be 0 right now. + // (Providing 0 would be a "proper" reset, but we want to resume parsing) + musl_optind = atFileStack.back().parentInd; + atFileStack.pop_back(); + if (atFileStack.empty()) { + curArgc = argc; + curArgv = argv; + } else { + std::vector &vec = atFileStack.back().argv; + curArgc = vec.size(); + curArgv = vec.data(); + } + } + } +} diff --git a/src/fix/fix.cpp b/src/fix/fix.cpp index cebd5d07..3c49b667 100644 --- a/src/fix/fix.cpp +++ b/src/fix/fix.cpp @@ -141,7 +141,11 @@ static void if (options.title) { overwriteBytes( - rom0, 0x134, reinterpret_cast(options.title), options.titleLen, "title" + rom0, + 0x134, + reinterpret_cast(options.title->c_str()), + options.titleLen, + "title" ); } @@ -149,7 +153,7 @@ static void overwriteBytes( rom0, 0x13F, - reinterpret_cast(options.gameID), + reinterpret_cast(options.gameID->c_str()), options.gameIDLen, "manufacturer code" ); @@ -163,7 +167,7 @@ static void overwriteBytes( rom0, 0x144, - reinterpret_cast(options.newLicensee), + reinterpret_cast(options.newLicensee->c_str()), options.newLicenseeLen, "new licensee code" ); diff --git a/src/fix/main.cpp b/src/fix/main.cpp index 50714f06..94ff78f3 100644 --- a/src/fix/main.cpp +++ b/src/fix/main.cpp @@ -12,8 +12,8 @@ #include #include +#include "cli.hpp" #include "diagnostics.hpp" -#include "extern/getopt.hpp" #include "helpers.hpp" #include "platform.hpp" #include "style.hpp" @@ -27,10 +27,16 @@ Options options; +// Flags which must be processed after the option parsing finishes +static struct LocalOptions { + std::optional outputFileName; // -o + std::vector inputFileNames; // ... +} localOptions; + // Short options static char const *optstring = "Ccf:hi:jk:L:l:m:n:Oo:p:r:st:VvW:w"; -// Variables for the long-only options +// Long-only option variable static int longOpt; // `--color` // Equivalent long options @@ -91,13 +97,191 @@ static Usage usage = { }; // clang-format on -static void parseByte(uint16_t &output, char name) { - if (std::optional value = parseWholeNumber(musl_optarg); !value) { +static uint16_t parseByte(char const *input, char name) { + if (std::optional value = parseWholeNumber(input); !value) { fatal("Invalid argument for option '-%c'", name); } else if (*value > 0xFF) { fatal("Argument for option '-%c' must be between 0 and 0xFF", name); } else { - output = *value; + return *value; + } +} + +static void parseArg(int ch, char *arg) { + switch (ch) { + case 'C': + case 'c': + options.model = ch == 'c' ? BOTH : CGB; + if (options.titleLen > 15) { + options.titleLen = 15; + assume(options.title.has_value()); + warning( + WARNING_TRUNCATION, "Truncating title \"%s\" to 15 chars", options.title->c_str() + ); + } + break; + + case 'f': + options.fixSpec = 0; + while (*arg) { + switch (*arg) { +#define overrideSpec(cur, bad, curFlag, badFlag) \ + case cur: \ + if (options.fixSpec & badFlag) { \ + warnx("'%c' overriding '%c' in fix spec", cur, bad); \ + } \ + options.fixSpec = (options.fixSpec & ~badFlag) | curFlag; \ + break +#define overrideSpecPair(fix, fixFlag, trash, trashFlag) \ + overrideSpec(fix, trash, fixFlag, trashFlag); \ + overrideSpec(trash, fix, trashFlag, fixFlag) + overrideSpecPair('l', FIX_LOGO, 'L', TRASH_LOGO); + overrideSpecPair('h', FIX_HEADER_SUM, 'H', TRASH_HEADER_SUM); + overrideSpecPair('g', FIX_GLOBAL_SUM, 'G', TRASH_GLOBAL_SUM); +#undef overrideSpec +#undef overrideSpecPair + + default: + fatal("Invalid character '%c' in fix spec", *arg); + } + ++arg; + } + break; + + // LCOV_EXCL_START + case 'h': + usage.printAndExit(0); + // LCOV_EXCL_STOP + + case 'i': { + options.gameID = arg; + size_t len = options.gameID->length(); + if (len > 4) { + len = 4; + warning( + WARNING_TRUNCATION, "Truncating game ID \"%s\" to 4 chars", options.gameID->c_str() + ); + } + options.gameIDLen = len; + if (options.titleLen > 11) { + options.titleLen = 11; + assume(options.title.has_value()); + warning( + WARNING_TRUNCATION, "Truncating title \"%s\" to 11 chars", options.title->c_str() + ); + } + break; + } + + case 'j': + options.japanese = false; + break; + + case 'k': { + options.newLicensee = arg; + size_t len = options.newLicensee->length(); + if (len > 2) { + len = 2; + warning( + WARNING_TRUNCATION, + "Truncating new licensee \"%s\" to 2 chars", + options.newLicensee->c_str() + ); + } + options.newLicenseeLen = len; + break; + } + + case 'L': + options.logoFilename = arg; + break; + + case 'l': + options.oldLicensee = parseByte(arg, 'l'); + break; + + case 'm': + options.cartridgeType = mbc_ParseName(arg, options.tpp1Rev[0], options.tpp1Rev[1]); + if (options.cartridgeType == ROM_RAM || options.cartridgeType == ROM_RAM_BATTERY) { + warning(WARNING_MBC, "MBC \"%s\" is under-specified and poorly supported", arg); + } + break; + + case 'n': + options.romVersion = parseByte(arg, 'n'); + break; + + case 'O': + warning(WARNING_OBSOLETE, "'-O' is deprecated; use '-Wno-overwrite' instead"); + warnings.processWarningFlag("no-overwrite"); + break; + + case 'o': + localOptions.outputFileName = arg; + break; + + case 'p': + options.padValue = parseByte(arg, 'p'); + break; + + case 'r': + options.ramSize = parseByte(arg, 'r'); + break; + + case 's': + options.sgb = true; + break; + + case 't': { + options.title = arg; + size_t len = options.title->length(); + uint8_t maxLen = options.gameID ? 11 : options.model != DMG ? 15 : 16; + + if (len > maxLen) { + len = maxLen; + warning( + WARNING_TRUNCATION, + "Truncating title \"%s\" to %u chars", + options.title->c_str(), + maxLen + ); + } + options.titleLen = len; + break; + } + + // LCOV_EXCL_START + case 'V': + printf("rgbfix %s\n", get_package_version_string()); + exit(0); + + case 'v': + options.fixSpec = FIX_LOGO | FIX_HEADER_SUM | FIX_GLOBAL_SUM; + break; + // LCOV_EXCL_STOP + + case 'W': + warnings.processWarningFlag(arg); + break; + + case 'w': + warnings.state.warningsEnabled = false; + break; + + case 0: // Long-only options + if (longOpt == 'c' && !style_Parse(arg)) { + fatal("Invalid argument for option '--color'"); + } + break; + + case 1: // Positional arguments + localOptions.inputFileNames.push_back(arg); + break; + + // LCOV_EXCL_START + default: + usage.printAndExit(1); + // LCOV_EXCL_STOP } } @@ -110,18 +294,19 @@ static uint8_t const nintendoLogo[] = { static void initLogo() { if (options.logoFilename) { FILE *logoFile; - if (strcmp(options.logoFilename, "-")) { - logoFile = fopen(options.logoFilename, "rb"); + char const *logoFilename = options.logoFilename->c_str(); + if (*options.logoFilename != "-") { + logoFile = fopen(logoFilename, "rb"); } else { // LCOV_EXCL_START - options.logoFilename = ""; + logoFilename = ""; (void)setmode(STDIN_FILENO, O_BINARY); logoFile = stdin; // LCOV_EXCL_STOP } if (!logoFile) { // LCOV_EXCL_START - fatal("Failed to open \"%s\" for reading: %s", options.logoFilename, strerror(errno)); + fatal("Failed to open \"%s\" for reading: %s", logoFilename, strerror(errno)); // LCOV_EXCL_STOP } Defer closeLogo{[&] { fclose(logoFile); }}; @@ -129,7 +314,7 @@ static void initLogo() { uint8_t logoBpp[sizeof(options.logo)]; if (size_t nbRead = fread(logoBpp, 1, sizeof(logoBpp), logoFile); nbRead != sizeof(options.logo) || fgetc(logoFile) != EOF || ferror(logoFile)) { - fatal("\"%s\" is not %zu bytes", options.logoFilename, sizeof(options.logo)); + fatal("\"%s\" is not %zu bytes", logoFilename, sizeof(options.logo)); } auto highs = [&logoBpp](size_t i) { return (logoBpp[i * 2] & 0xF0) | ((logoBpp[i * 2 + 1] & 0xF0) >> 4); @@ -161,176 +346,7 @@ static void initLogo() { } int main(int argc, char *argv[]) { - char const *outputFilename = nullptr; - - // Parse CLI options - for (int ch; (ch = musl_getopt_long_only(argc, argv, optstring, longopts, nullptr)) != -1;) { - switch (ch) { - case 'C': - case 'c': - options.model = ch == 'c' ? BOTH : CGB; - if (options.titleLen > 15) { - options.titleLen = 15; - assume(options.title != nullptr); - warning(WARNING_TRUNCATION, "Truncating title \"%s\" to 15 chars", options.title); - } - break; - - case 'f': - options.fixSpec = 0; - while (*musl_optarg) { - switch (*musl_optarg) { -#define overrideSpec(cur, bad, curFlag, badFlag) \ - case cur: \ - if (options.fixSpec & badFlag) { \ - warnx("'%c' overriding '%c' in fix spec", cur, bad); \ - } \ - options.fixSpec = (options.fixSpec & ~badFlag) | curFlag; \ - break -#define overrideSpecPair(fix, fixFlag, trash, trashFlag) \ - overrideSpec(fix, trash, fixFlag, trashFlag); \ - overrideSpec(trash, fix, trashFlag, fixFlag) - overrideSpecPair('l', FIX_LOGO, 'L', TRASH_LOGO); - overrideSpecPair('h', FIX_HEADER_SUM, 'H', TRASH_HEADER_SUM); - overrideSpecPair('g', FIX_GLOBAL_SUM, 'G', TRASH_GLOBAL_SUM); -#undef overrideSpec -#undef overrideSpecPair - - default: - fatal("Invalid character '%c' in fix spec", *musl_optarg); - } - ++musl_optarg; - } - break; - - // LCOV_EXCL_START - case 'h': - usage.printAndExit(0); - // LCOV_EXCL_STOP - - case 'i': { - options.gameID = musl_optarg; - size_t len = strlen(options.gameID); - if (len > 4) { - len = 4; - warning(WARNING_TRUNCATION, "Truncating game ID \"%s\" to 4 chars", options.gameID); - } - options.gameIDLen = len; - if (options.titleLen > 11) { - options.titleLen = 11; - assume(options.title != nullptr); - warning(WARNING_TRUNCATION, "Truncating title \"%s\" to 11 chars", options.title); - } - break; - } - - case 'j': - options.japanese = false; - break; - - case 'k': { - options.newLicensee = musl_optarg; - size_t len = strlen(options.newLicensee); - if (len > 2) { - len = 2; - warning( - WARNING_TRUNCATION, - "Truncating new licensee \"%s\" to 2 chars", - options.newLicensee - ); - } - options.newLicenseeLen = len; - break; - } - - case 'L': - options.logoFilename = musl_optarg; - break; - - case 'l': - parseByte(options.oldLicensee, 'l'); - break; - - case 'm': - options.cartridgeType = - mbc_ParseName(musl_optarg, options.tpp1Rev[0], options.tpp1Rev[1]); - if (options.cartridgeType == ROM_RAM || options.cartridgeType == ROM_RAM_BATTERY) { - warning( - WARNING_MBC, "MBC \"%s\" is under-specified and poorly supported", musl_optarg - ); - } - break; - - case 'n': - parseByte(options.romVersion, 'n'); - break; - - case 'O': - warning(WARNING_OBSOLETE, "'-O' is deprecated; use '-Wno-overwrite' instead"); - warnings.processWarningFlag("no-overwrite"); - break; - - case 'o': - outputFilename = musl_optarg; - break; - - case 'p': - parseByte(options.padValue, 'p'); - break; - - case 'r': - parseByte(options.ramSize, 'r'); - break; - - case 's': - options.sgb = true; - break; - - case 't': { - options.title = musl_optarg; - size_t len = strlen(options.title); - uint8_t maxLen = options.gameID ? 11 : options.model != DMG ? 15 : 16; - - if (len > maxLen) { - len = maxLen; - warning( - WARNING_TRUNCATION, "Truncating title \"%s\" to %u chars", options.title, maxLen - ); - } - options.titleLen = len; - break; - } - - // LCOV_EXCL_START - case 'V': - printf("rgbfix %s\n", get_package_version_string()); - exit(0); - - case 'v': - options.fixSpec = FIX_LOGO | FIX_HEADER_SUM | FIX_GLOBAL_SUM; - break; - // LCOV_EXCL_STOP - - case 'W': - warnings.processWarningFlag(musl_optarg); - break; - - case 'w': - warnings.state.warningsEnabled = false; - break; - - case 0: // Long-only options - if (longOpt == 'c' && !style_Parse(musl_optarg)) { - fatal("Invalid argument for option '--color'"); - } - break; - - // LCOV_EXCL_START - default: - usage.printAndExit(1); - // LCOV_EXCL_STOP - } - } + cli_ParseArgs(argc, argv, optstring, longopts, parseArg, fatal); if ((options.cartridgeType & 0xFF00) == TPP1 && !options.japanese) { warning( @@ -382,19 +398,20 @@ int main(int argc, char *argv[]) { initLogo(); - argv += musl_optind; - if (!*argv) { + if (localOptions.inputFileNames.empty()) { usage.printAndExit("No input file specified (pass \"-\" to read from standard input)"); } - if (outputFilename && argc != musl_optind + 1) { + if (localOptions.outputFileName && localOptions.inputFileNames.size() != 1) { usage.printAndExit("If '-o' is set then only a single input file may be specified"); } + char const *outputFileName = + localOptions.outputFileName ? localOptions.outputFileName->c_str() : nullptr; bool failed = warnings.nbErrors > 0; - do { - failed |= fix_ProcessFile(*argv, outputFilename); - } while (*++argv); + for (std::string const &inputFileName : localOptions.inputFileNames) { + failed |= fix_ProcessFile(inputFileName.c_str(), outputFileName); + } return failed; } diff --git a/src/gfx/main.cpp b/src/gfx/main.cpp index 22d16975..9b0dc945 100644 --- a/src/gfx/main.cpp +++ b/src/gfx/main.cpp @@ -2,7 +2,6 @@ #include "gfx/main.hpp" -#include #include #include #include @@ -11,11 +10,12 @@ #include #include #include +#include #include #include +#include "cli.hpp" #include "diagnostics.hpp" -#include "extern/getopt.hpp" #include "file.hpp" #include "helpers.hpp" #include "platform.hpp" @@ -35,22 +35,23 @@ using namespace std::literals::string_view_literals; Options options; +// Flags which must be processed after the option parsing finishes static struct LocalOptions { - char const *externalPalSpec; - bool autoAttrmap; - bool autoTilemap; - bool autoPalettes; - bool autoPalmap; - bool groupOutputs; - bool reverse; + std::optional externalPalSpec; // -c + bool autoAttrmap; // -A + bool autoTilemap; // -T + bool autoPalettes; // -P + bool autoPalmap; // -Q + bool groupOutputs; // -O + bool reverse; // -r bool autoAny() const { return autoAttrmap || autoTilemap || autoPalettes || autoPalmap; } } localOptions; // Short options -static char const *optstring = "-Aa:B:b:Cc:d:hi:L:l:mN:n:Oo:Pp:Qq:r:s:Tt:U:uVvW:wXx:YZ"; +static char const *optstring = "Aa:B:b:Cc:d:hi:L:l:mN:n:Oo:Pp:Qq:r:s:Tt:U:uVvW:wXx:YZ"; -// Variables for the long-only options +// Long-only option variable static int longOpt; // `--color` // Equivalent long options @@ -137,418 +138,339 @@ static void skipBlankSpace(char const *&arg) { arg += strspn(arg, " \t"); } -static void registerInput(char const *arg) { - if (!options.input.empty()) { - usage.printAndExit( - "Input image specified more than once! (first \"%s\", then \"%s\")", - options.input.c_str(), - arg - ); - } else if (arg[0] == '\0') { // Empty input path - usage.printAndExit("Input image path cannot be empty"); - } else { - options.input = arg; - } -} +static void parseArg(int ch, char *arg) { + char const *argPtr = arg; // Make a copy for scanning -// Turn an at-file's contents into an argv that `getopt` can handle, appending them to `argPool`. -static std::vector readAtFile(std::string const &path, std::vector &argPool) { - File file; - if (!file.open(path, std::ios_base::in)) { - fatal("Error reading at-file \"%s\": %s", file.c_str(path), strerror(errno)); + switch (ch) { + case 'A': + localOptions.autoAttrmap = true; + break; + + case 'a': + localOptions.autoAttrmap = false; + if (!options.attrmap.empty()) { + warnx("Overriding attrmap file \"%s\"", options.attrmap.c_str()); + } + options.attrmap = arg; + break; + + case 'B': + parseBackgroundPalSpec(arg); + break; + + case 'b': { + uint16_t number = readNumber(argPtr, "Bank 0 base tile ID", 0); + if (number >= 256) { + error("Bank 0 base tile ID must be below 256"); + } else { + options.baseTileIDs[0] = number; + } + if (*argPtr == '\0') { + options.baseTileIDs[1] = 0; + break; + } + skipBlankSpace(argPtr); + if (*argPtr != ',') { + error("Base tile IDs must be one or two comma-separated numbers, not \"%s\"", arg); + break; + } + ++argPtr; // Skip comma + skipBlankSpace(argPtr); + number = readNumber(argPtr, "Bank 1 base tile ID", 0); + if (number >= 256) { + error("Bank 1 base tile ID must be below 256"); + } else { + options.baseTileIDs[1] = number; + } + if (*argPtr != '\0') { + error("Base tile IDs must be one or two comma-separated numbers, not \"%s\"", arg); + break; + } + break; } - for (std::vector argvOfs;;) { - int c = file->sbumpc(); + case 'C': + options.useColorCurve = true; + break; - // First, discard any leading blank space - while (isBlankSpace(c)) { - c = file->sbumpc(); + case 'c': + localOptions.externalPalSpec = std::nullopt; // Allow overriding a previous pal spec + if (arg[0] == '#') { + options.palSpecType = Options::EXPLICIT; + parseInlinePalSpec(arg); + } else if (strcasecmp(arg, "embedded") == 0) { + // Use PLTE, error out if missing + options.palSpecType = Options::EMBEDDED; + } else if (strcasecmp(arg, "auto") == 0) { + options.palSpecType = Options::NO_SPEC; + } else if (strcasecmp(arg, "dmg") == 0) { + options.palSpecType = Options::DMG; + parseDmgPalSpec(0xE4); // Same darkest-first order as `sortGrayscale` + } else if (strncasecmp(arg, "dmg=", literal_strlen("dmg=")) == 0) { + options.palSpecType = Options::DMG; + parseDmgPalSpec(&arg[literal_strlen("dmg=")]); + } else { + options.palSpecType = Options::EXPLICIT; + localOptions.externalPalSpec = arg; } + break; - // If it's a comment, discard everything until EOL - if (c == '#') { - c = file->sbumpc(); - while (c != EOF && !isNewline(c)) { - c = file->sbumpc(); - } + case 'd': + options.bitDepth = readNumber(argPtr, "Bit depth", 2); + if (*argPtr != '\0') { + error("Bit depth ('-b') argument must be a valid number, not \"%s\"", arg); + } else if (options.bitDepth != 1 && options.bitDepth != 2) { + error("Bit depth must be 1 or 2, not %" PRIu8, options.bitDepth); + options.bitDepth = 2; } + break; - if (c == EOF) { - return argvOfs; - } else if (isNewline(c)) { - continue; // Start processing the next line + // LCOV_EXCL_START + case 'h': + usage.printAndExit(0); + // LCOV_EXCL_STOP + + case 'i': + if (!options.inputTileset.empty()) { + warnx("Overriding input tileset file \"%s\"", options.inputTileset.c_str()); } + options.inputTileset = arg; + break; - // Alright, now we can parse the line - do { - argvOfs.push_back(argPool.size()); - - // Read one argument (until the next whitespace char). - // We know there is one because we already have its first character in `c`. - for (; c != EOF && !isWhitespace(c); c = file->sbumpc()) { - argPool.push_back(c); - } - argPool.push_back('\0'); - - // Discard blank space until the next argument (candidate) - while (isBlankSpace(c)) { - c = file->sbumpc(); - } - } while (c != EOF && !isNewline(c)); // End if we reached EOL - } -} - -// Parses an arg vector, modifying `options` and `localOptions` as options are read. -// The `localOptions` struct is for flags which must be processed after the option parsing finishes. -// Returns `nullptr` if the vector was fully parsed, or a pointer (which is part of the arg vector) -// to an "at-file" path if one is encountered. -static char *parseArgv(int argc, char *argv[]) { - for (int ch; (ch = musl_getopt_long_only(argc, argv, optstring, longopts, nullptr)) != -1;) { - char const *arg = musl_optarg; // Make a copy for scanning - - switch (ch) { - case 'A': - localOptions.autoAttrmap = true; - break; - - case 'a': - localOptions.autoAttrmap = false; - if (!options.attrmap.empty()) { - warnx("Overriding attrmap file \"%s\"", options.attrmap.c_str()); - } - options.attrmap = musl_optarg; - break; - - case 'B': - parseBackgroundPalSpec(musl_optarg); - break; - - case 'b': { - uint16_t number = readNumber(arg, "Bank 0 base tile ID", 0); - if (number >= 256) { - error("Bank 0 base tile ID must be below 256"); - } else { - options.baseTileIDs[0] = number; - } - if (*arg == '\0') { - options.baseTileIDs[1] = 0; - break; - } - skipBlankSpace(arg); - if (*arg != ',') { - error( - "Base tile IDs must be one or two comma-separated numbers, not \"%s\"", - musl_optarg - ); - break; - } - ++arg; // Skip comma - skipBlankSpace(arg); - number = readNumber(arg, "Bank 1 base tile ID", 0); - if (number >= 256) { - error("Bank 1 base tile ID must be below 256"); - } else { - options.baseTileIDs[1] = number; - } - if (*arg != '\0') { - error( - "Base tile IDs must be one or two comma-separated numbers, not \"%s\"", - musl_optarg - ); - break; - } + case 'L': + options.inputSlice.left = readNumber(argPtr, "Input slice left coordinate"); + if (options.inputSlice.left > INT16_MAX) { + error("Input slice left coordinate is out of range!"); break; } - - case 'C': - options.useColorCurve = true; - break; - - case 'c': - localOptions.externalPalSpec = nullptr; // Allow overriding a previous pal spec - if (musl_optarg[0] == '#') { - options.palSpecType = Options::EXPLICIT; - parseInlinePalSpec(musl_optarg); - } else if (strcasecmp(musl_optarg, "embedded") == 0) { - // Use PLTE, error out if missing - options.palSpecType = Options::EMBEDDED; - } else if (strcasecmp(musl_optarg, "auto") == 0) { - options.palSpecType = Options::NO_SPEC; - } else if (strcasecmp(musl_optarg, "dmg") == 0) { - options.palSpecType = Options::DMG; - parseDmgPalSpec(0xE4); // Same darkest-first order as `sortGrayscale` - } else if (strncasecmp(musl_optarg, "dmg=", literal_strlen("dmg=")) == 0) { - options.palSpecType = Options::DMG; - parseDmgPalSpec(&musl_optarg[literal_strlen("dmg=")]); - } else { - options.palSpecType = Options::EXPLICIT; - localOptions.externalPalSpec = musl_optarg; - } - break; - - case 'd': - options.bitDepth = readNumber(arg, "Bit depth", 2); - if (*arg != '\0') { - error("Bit depth ('-b') argument must be a valid number, not \"%s\"", musl_optarg); - } else if (options.bitDepth != 1 && options.bitDepth != 2) { - error("Bit depth must be 1 or 2, not %" PRIu8, options.bitDepth); - options.bitDepth = 2; - } - break; - - // LCOV_EXCL_START - case 'h': - usage.printAndExit(0); - // LCOV_EXCL_STOP - - case 'i': - if (!options.inputTileset.empty()) { - warnx("Overriding input tileset file \"%s\"", options.inputTileset.c_str()); - } - options.inputTileset = musl_optarg; - break; - - case 'L': - options.inputSlice.left = readNumber(arg, "Input slice left coordinate"); - if (options.inputSlice.left > INT16_MAX) { - error("Input slice left coordinate is out of range!"); - break; - } - skipBlankSpace(arg); - if (*arg != ',') { - error("Missing comma after left coordinate in \"%s\"", musl_optarg); - break; - } - ++arg; - skipBlankSpace(arg); - options.inputSlice.top = readNumber(arg, "Input slice upper coordinate"); - skipBlankSpace(arg); - if (*arg != ':') { - error("Missing colon after upper coordinate in \"%s\"", musl_optarg); - break; - } - ++arg; - skipBlankSpace(arg); - options.inputSlice.width = readNumber(arg, "Input slice width"); - skipBlankSpace(arg); - if (options.inputSlice.width == 0) { - error("Input slice width may not be 0!"); - } - if (*arg != ',') { - error("Missing comma after width in \"%s\"", musl_optarg); - break; - } - ++arg; - skipBlankSpace(arg); - options.inputSlice.height = readNumber(arg, "Input slice height"); - if (options.inputSlice.height == 0) { - error("Input slice height may not be 0!"); - } - if (*arg != '\0') { - error("Unexpected extra characters after slice spec in \"%s\"", musl_optarg); - } - break; - - case 'l': { - uint16_t number = readNumber(arg, "Base palette ID", 0); - if (*arg != '\0') { - error("Base palette ID must be a valid number, not \"%s\"", musl_optarg); - } else if (number >= 256) { - error("Base palette ID must be below 256"); - } else { - options.basePalID = number; - } + skipBlankSpace(argPtr); + if (*argPtr != ',') { + error("Missing comma after left coordinate in \"%s\"", arg); break; } - - case 'm': - options.allowMirroringX = true; // Imply `-X` - options.allowMirroringY = true; // Imply `-Y` - [[fallthrough]]; // Imply `-u` - - case 'u': - options.allowDedup = true; - break; - - case 'N': - options.maxNbTiles[0] = readNumber(arg, "Number of tiles in bank 0", 256); - if (options.maxNbTiles[0] > 256) { - error("Bank 0 cannot contain more than 256 tiles"); - } - if (*arg == '\0') { - options.maxNbTiles[1] = 0; - break; - } - skipBlankSpace(arg); - if (*arg != ',') { - error( - "Bank capacity must be one or two comma-separated numbers, not \"%s\"", - musl_optarg - ); - break; - } - ++arg; // Skip comma - skipBlankSpace(arg); - options.maxNbTiles[1] = readNumber(arg, "Number of tiles in bank 1", 256); - if (options.maxNbTiles[1] > 256) { - error("Bank 1 cannot contain more than 256 tiles"); - } - if (*arg != '\0') { - error( - "Bank capacity must be one or two comma-separated numbers, not \"%s\"", - musl_optarg - ); - break; - } - break; - - case 'n': { - uint16_t number = readNumber(arg, "Number of palettes", 256); - if (*arg != '\0') { - error("Number of palettes ('-n') must be a valid number, not \"%s\"", musl_optarg); - } - if (number > 256) { - error("Number of palettes ('-n') must not exceed 256!"); - } else if (number == 0) { - error("Number of palettes ('-n') may not be 0!"); - } else { - options.nbPalettes = number; - } + ++argPtr; + skipBlankSpace(argPtr); + options.inputSlice.top = readNumber(argPtr, "Input slice upper coordinate"); + skipBlankSpace(argPtr); + if (*argPtr != ':') { + error("Missing colon after upper coordinate in \"%s\"", arg); break; } - - case 'O': - localOptions.groupOutputs = true; - break; - - case 'o': - if (!options.output.empty()) { - warnx("Overriding tile data file %s", options.output.c_str()); - } - options.output = musl_optarg; - break; - - case 'P': - localOptions.autoPalettes = true; - break; - - case 'p': - localOptions.autoPalettes = false; - if (!options.palettes.empty()) { - warnx("Overriding palettes file %s", options.palettes.c_str()); - } - options.palettes = musl_optarg; - break; - - case 'Q': - localOptions.autoPalmap = true; - break; - - case 'q': - localOptions.autoPalmap = false; - if (!options.palmap.empty()) { - warnx("Overriding palette map file %s", options.palmap.c_str()); - } - options.palmap = musl_optarg; - break; - - case 'r': - localOptions.reverse = true; - options.reversedWidth = readNumber(arg, "Reversed image stride"); - if (*arg != '\0') { - error( - "Reversed image stride ('-r') must be a valid number, not \"%s\"", musl_optarg - ); - } - break; - - case 's': - options.nbColorsPerPal = readNumber(arg, "Number of colors per palette", 4); - if (*arg != '\0') { - error("Palette size ('-s') must be a valid number, not \"%s\"", musl_optarg); - } - if (options.nbColorsPerPal > 4) { - error("Palette size ('-s') must not exceed 4!"); - } else if (options.nbColorsPerPal == 0) { - error("Palette size ('-s') may not be 0!"); - } - break; - - case 'T': - localOptions.autoTilemap = true; - break; - - case 't': - localOptions.autoTilemap = false; - if (!options.tilemap.empty()) { - warnx("Overriding tilemap file %s", options.tilemap.c_str()); - } - options.tilemap = musl_optarg; - break; - - // LCOV_EXCL_START - case 'V': - printf("rgbgfx %s\n", get_package_version_string()); - exit(0); - - case 'v': - incrementVerbosity(); - break; - // LCOV_EXCL_STOP - - case 'W': - warnings.processWarningFlag(musl_optarg); - break; - - case 'w': - warnings.state.warningsEnabled = false; - break; - - case 'x': - options.trim = readNumber(arg, "Number of tiles to trim", 0); - if (*arg != '\0') { - error("Tile trim ('-x') argument must be a valid number, not \"%s\"", musl_optarg); - } - break; - - case 'X': - options.allowMirroringX = true; - options.allowDedup = true; // Imply `-u` - break; - - case 'Y': - options.allowMirroringY = true; - options.allowDedup = true; // Imply `-u` - break; - - case 'Z': - options.columnMajor = true; - break; - - case 0: // Long-only options - if (longOpt == 'c' && !style_Parse(musl_optarg)) { - fatal("Invalid argument for option '--color'"); - } - break; - - case 1: // Positional argument, requested by leading `-` in opt string - if (musl_optarg[0] == '@') { - // Instruct the caller to process that at-file - return &musl_optarg[1]; - } else { - registerInput(musl_optarg); - } - break; - - // LCOV_EXCL_START - default: - usage.printAndExit(1); - // LCOV_EXCL_STOP + ++argPtr; + skipBlankSpace(argPtr); + options.inputSlice.width = readNumber(argPtr, "Input slice width"); + skipBlankSpace(argPtr); + if (options.inputSlice.width == 0) { + error("Input slice width may not be 0!"); } + if (*argPtr != ',') { + error("Missing comma after width in \"%s\"", arg); + break; + } + ++argPtr; + skipBlankSpace(argPtr); + options.inputSlice.height = readNumber(argPtr, "Input slice height"); + if (options.inputSlice.height == 0) { + error("Input slice height may not be 0!"); + } + if (*argPtr != '\0') { + error("Unexpected extra characters after slice spec in \"%s\"", arg); + } + break; + + case 'l': { + uint16_t number = readNumber(argPtr, "Base palette ID", 0); + if (*argPtr != '\0') { + error("Base palette ID must be a valid number, not \"%s\"", arg); + } else if (number >= 256) { + error("Base palette ID must be below 256"); + } else { + options.basePalID = number; + } + break; } - return nullptr; // Done processing this argv + case 'm': + options.allowMirroringX = true; // Imply `-X` + options.allowMirroringY = true; // Imply `-Y` + [[fallthrough]]; // Imply `-u` + + case 'u': + options.allowDedup = true; + break; + + case 'N': + options.maxNbTiles[0] = readNumber(argPtr, "Number of tiles in bank 0", 256); + if (options.maxNbTiles[0] > 256) { + error("Bank 0 cannot contain more than 256 tiles"); + } + if (*argPtr == '\0') { + options.maxNbTiles[1] = 0; + break; + } + skipBlankSpace(argPtr); + if (*argPtr != ',') { + error("Bank capacity must be one or two comma-separated numbers, not \"%s\"", arg); + break; + } + ++argPtr; // Skip comma + skipBlankSpace(argPtr); + options.maxNbTiles[1] = readNumber(argPtr, "Number of tiles in bank 1", 256); + if (options.maxNbTiles[1] > 256) { + error("Bank 1 cannot contain more than 256 tiles"); + } + if (*argPtr != '\0') { + error("Bank capacity must be one or two comma-separated numbers, not \"%s\"", arg); + break; + } + break; + + case 'n': { + uint16_t number = readNumber(argPtr, "Number of palettes", 256); + if (*argPtr != '\0') { + error("Number of palettes ('-n') must be a valid number, not \"%s\"", arg); + } + if (number > 256) { + error("Number of palettes ('-n') must not exceed 256!"); + } else if (number == 0) { + error("Number of palettes ('-n') may not be 0!"); + } else { + options.nbPalettes = number; + } + break; + } + + case 'O': + localOptions.groupOutputs = true; + break; + + case 'o': + if (!options.output.empty()) { + warnx("Overriding tile data file %s", options.output.c_str()); + } + options.output = arg; + break; + + case 'P': + localOptions.autoPalettes = true; + break; + + case 'p': + localOptions.autoPalettes = false; + if (!options.palettes.empty()) { + warnx("Overriding palettes file %s", options.palettes.c_str()); + } + options.palettes = arg; + break; + + case 'Q': + localOptions.autoPalmap = true; + break; + + case 'q': + localOptions.autoPalmap = false; + if (!options.palmap.empty()) { + warnx("Overriding palette map file %s", options.palmap.c_str()); + } + options.palmap = arg; + break; + + case 'r': + localOptions.reverse = true; + options.reversedWidth = readNumber(argPtr, "Reversed image stride"); + if (*argPtr != '\0') { + error("Reversed image stride ('-r') must be a valid number, not \"%s\"", arg); + } + break; + + case 's': + options.nbColorsPerPal = readNumber(argPtr, "Number of colors per palette", 4); + if (*argPtr != '\0') { + error("Palette size ('-s') must be a valid number, not \"%s\"", arg); + } + if (options.nbColorsPerPal > 4) { + error("Palette size ('-s') must not exceed 4!"); + } else if (options.nbColorsPerPal == 0) { + error("Palette size ('-s') may not be 0!"); + } + break; + + case 'T': + localOptions.autoTilemap = true; + break; + + case 't': + localOptions.autoTilemap = false; + if (!options.tilemap.empty()) { + warnx("Overriding tilemap file %s", options.tilemap.c_str()); + } + options.tilemap = arg; + break; + + // LCOV_EXCL_START + case 'V': + printf("rgbgfx %s\n", get_package_version_string()); + exit(0); + + case 'v': + incrementVerbosity(); + break; + // LCOV_EXCL_STOP + + case 'W': + warnings.processWarningFlag(arg); + break; + + case 'w': + warnings.state.warningsEnabled = false; + break; + + case 'x': + options.trim = readNumber(argPtr, "Number of tiles to trim", 0); + if (*argPtr != '\0') { + error("Tile trim ('-x') argument must be a valid number, not \"%s\"", arg); + } + break; + + case 'X': + options.allowMirroringX = true; + options.allowDedup = true; // Imply `-u` + break; + + case 'Y': + options.allowMirroringY = true; + options.allowDedup = true; // Imply `-u` + break; + + case 'Z': + options.columnMajor = true; + break; + + case 0: // Long-only options + if (longOpt == 'c' && !style_Parse(arg)) { + fatal("Invalid argument for option '--color'"); + } + break; + + case 1: // Positional argument + if (!options.input.empty()) { + usage.printAndExit( + "Input image specified more than once! (first \"%s\", then \"%s\")", + options.input.c_str(), + arg + ); + } else if (arg[0] == '\0') { // Empty input path + usage.printAndExit("Input image path cannot be empty"); + } else { + options.input = arg; + } + break; + + // LCOV_EXCL_START + default: + usage.printAndExit(1); + // LCOV_EXCL_STOP + } } // LCOV_EXCL_START @@ -717,70 +639,7 @@ static void replaceExtension(std::string &path, char const *extension) { } int main(int argc, char *argv[]) { - struct AtFileStackEntry { - int parentInd; // Saved offset into parent argv - std::vector argv; // This context's arg pointer vec - - AtFileStackEntry(int parentInd_, std::vector argv_) - : parentInd(parentInd_), argv(argv_) {} - }; - std::vector atFileStack; - - // Parse CLI options - int curArgc = argc; - char **curArgv = argv; - std::vector> argPools; - for (;;) { - char *atFileName = parseArgv(curArgc, curArgv); - if (atFileName) { - // We need to allocate a new arg pool for each at-file, so as not to invalidate pointers - // previous at-files may have generated to their own arg pools. - // But for the same reason, the arg pool must also outlive the at-file's stack entry! - std::vector &argPool = argPools.emplace_back(); - - // Copy `argv[0]` for error reporting, and because option parsing skips it - AtFileStackEntry &stackEntry = - atFileStack.emplace_back(musl_optind, std::vector{atFileName}); - // It would be nice to compute the char pointers on the fly, but reallocs don't allow - // that; so we must compute the offsets after the pool is fixed - std::vector offsets = readAtFile(&musl_optarg[1], argPool); - stackEntry.argv.reserve(offsets.size() + 2); // Avoid a bunch of reallocs - for (size_t ofs : offsets) { - stackEntry.argv.push_back(&argPool.data()[ofs]); - } - stackEntry.argv.push_back(nullptr); // Don't forget the arg vector terminator! - - curArgc = stackEntry.argv.size() - 1; - curArgv = stackEntry.argv.data(); - musl_optind = 1; // Don't use 0 because we're not scanning a different argv per se - continue; // Begin scanning that arg vector - } - - if (musl_optind != curArgc) { - // This happens if `--` is passed, process the remaining arg(s) as positional - assume(musl_optind < curArgc); - for (int i = musl_optind; i < curArgc; ++i) { - registerInput(argv[i]); - } - } - - // Pop off the top stack entry, or end parsing if none - if (atFileStack.empty()) { - break; - } - // OK to restore `optind` directly, because `optpos` must be 0 right now. - // (Providing 0 would be a "proper" reset, but we want to resume parsing) - musl_optind = atFileStack.back().parentInd; - atFileStack.pop_back(); - if (atFileStack.empty()) { - curArgc = argc; - curArgv = argv; - } else { - std::vector &vec = atFileStack.back().argv; - curArgc = vec.size(); - curArgv = vec.data(); - } - } + cli_ParseArgs(argc, argv, optstring, longopts, parseArg, fatal); if (options.nbColorsPerPal == 0) { options.nbColorsPerPal = 1u << options.bitDepth; @@ -823,7 +682,7 @@ int main(int argc, char *argv[]) { // Execute deferred external pal spec parsing, now that all other params are known if (localOptions.externalPalSpec) { - parseExternalPalSpec(localOptions.externalPalSpec); + parseExternalPalSpec(localOptions.externalPalSpec->c_str()); } verboseOutputConfig(); // LCOV_EXCL_LINE diff --git a/src/link/lexer.cpp b/src/link/lexer.cpp index 9f6fcf28..4ac24002 100644 --- a/src/link/lexer.cpp +++ b/src/link/lexer.cpp @@ -307,10 +307,10 @@ yy::parser::symbol_type yylex() { // Not marking as unreachable; this will generate a warning if any codepath forgets to return. } -bool lexer_Init(char const *linkerScriptName) { +bool lexer_Init(std::string const &linkerScriptName) { if (LexerStackEntry &newContext = lexerStack.emplace_back(std::string(linkerScriptName)); !newContext.file.open(newContext.path, std::ios_base::in)) { - error("Failed to open linker script \"%s\"", linkerScriptName); + error("Failed to open linker script \"%s\"", linkerScriptName.c_str()); lexerStack.clear(); return false; } diff --git a/src/link/main.cpp b/src/link/main.cpp index e4533b4b..c71c1ed5 100644 --- a/src/link/main.cpp +++ b/src/link/main.cpp @@ -13,8 +13,8 @@ #include #include "backtrace.hpp" +#include "cli.hpp" #include "diagnostics.hpp" -#include "extern/getopt.hpp" #include "linkdefs.hpp" #include "script.hpp" // Generated from script.y #include "style.hpp" @@ -33,12 +33,16 @@ Options options; -static char const *linkerScriptName = nullptr; // -l +// Flags which must be processed after the option parsing finishes +static struct LocalOptions { + std::optional linkerScriptName; // -l + std::vector inputFileNames; // ... +} localOptions; // Short options static char const *optstring = "B:dhl:m:Mn:O:o:p:S:tVvW:wx"; -// Variables for the long-only options +// Long-only option variable static int longOpt; // `--color` // Equivalent long options @@ -90,97 +94,6 @@ static Usage usage = { }; // clang-format on -// LCOV_EXCL_START -static void verboseOutputConfig(int argc, char *argv[]) { - if (!checkVerbosity(VERB_CONFIG)) { - return; - } - - style_Set(stderr, STYLE_MAGENTA, false); - - fprintf(stderr, "rgblink %s\n", get_package_version_string()); - - printVVVVVVerbosity(); - - fputs("Options:\n", stderr); - // -d/--dmg - if (options.isDmgMode) { - fputs("\tDMG mode prohibits non-DMG section types\n", stderr); - } - // -t/--tiny - if (options.is32kMode) { - fputs("\tROM0 covers the full 32 KiB of ROM\n", stderr); - } - // -w/--wramx - if (options.isWRAM0Mode) { - fputs("\tWRAM0 covers the full 8 KiB of WRAM\n", stderr); - } - // -x/--nopad - if (options.disablePadding) { - fputs("\tNo padding at the end of the ROM file\n", stderr); - } - // -p/--pad - fprintf(stderr, "\tPad value: 0x%02" PRIx8 "\n", options.padValue); - // -S/--scramble - if (options.scrambleROMX || options.scrambleWRAMX || options.scrambleSRAM) { - fputs("\tScramble: ", stderr); - if (options.scrambleROMX) { - fprintf(stderr, "ROMX = %" PRIu16, options.scrambleROMX); - if (options.scrambleWRAMX || options.scrambleSRAM) { - fputs(", ", stderr); - } - } - if (options.scrambleWRAMX) { - fprintf(stderr, "WRAMX = %" PRIu16, options.scrambleWRAMX); - if (options.scrambleSRAM) { - fputs(", ", stderr); - } - } - if (options.scrambleSRAM) { - fprintf(stderr, "SRAM = %" PRIu16, options.scrambleSRAM); - } - putc('\n', stderr); - } - // file ... - if (musl_optind < argc) { - fprintf(stderr, "\tInput object files: "); - for (int i = musl_optind; i < argc; ++i) { - if (i > musl_optind) { - fputs(", ", stderr); - } - if (i - musl_optind == 10) { - fprintf(stderr, "and %d more", argc - i); - break; - } - fputs(argv[i], stderr); - } - putc('\n', stderr); - } - auto printPath = [](char const *name, char const *path) { - if (path) { - fprintf(stderr, "\t%s: %s\n", name, path); - } - }; - // -O/--overlay - printPath("Overlay file", options.overlayFileName); - // -l/--linkerscript - printPath("Linker script", linkerScriptName); - // -o/--output - printPath("Output ROM file", options.outputFileName); - // -m/--map - printPath("Output map file", options.mapFileName); - // -M/--no-sym-in-map - if (options.mapFileName && options.noSymInMap) { - fputs("\tNo symbols in map file\n", stderr); - } - // -n/--sym - printPath("Output sym file", options.symFileName); - fputs("Ready.\n", stderr); - - style_Reset(stderr); -} -// LCOV_EXCL_STOP - static size_t skipBlankSpace(char const *str) { return strspn(str, " \t"); } @@ -292,124 +205,221 @@ static void parseScrambleSpec(char *spec) { } } -int main(int argc, char *argv[]) { - // Parse CLI options - for (int ch; (ch = musl_getopt_long_only(argc, argv, optstring, longopts, nullptr)) != -1;) { - switch (ch) { - case 'B': - if (!trace_ParseTraceDepth(musl_optarg)) { - fatal("Invalid argument for option '-B'"); - } - break; - - case 'd': - options.isDmgMode = true; - options.isWRAM0Mode = true; - break; - - // LCOV_EXCL_START - case 'h': - usage.printAndExit(0); - // LCOV_EXCL_STOP - - case 'l': - if (linkerScriptName) { - warnx("Overriding linker script file \"%s\"", linkerScriptName); - } - linkerScriptName = musl_optarg; - break; - - case 'M': - options.noSymInMap = true; - break; - - case 'm': - if (options.mapFileName) { - warnx("Overriding map file \"%s\"", options.mapFileName); - } - options.mapFileName = musl_optarg; - break; - - case 'n': - if (options.symFileName) { - warnx("Overriding sym file \"%s\"", options.symFileName); - } - options.symFileName = musl_optarg; - break; - - case 'O': - if (options.overlayFileName) { - warnx("Overriding overlay file \"%s\"", options.overlayFileName); - } - options.overlayFileName = musl_optarg; - break; - - case 'o': - if (options.outputFileName) { - warnx("Overriding output file \"%s\"", options.outputFileName); - } - options.outputFileName = musl_optarg; - break; - - case 'p': - if (std::optional value = parseWholeNumber(musl_optarg); !value) { - fatal("Invalid argument for option '-p'"); - } else if (*value > 0xFF) { - fatal("Argument for option '-p' must be between 0 and 0xFF"); - } else { - options.padValue = *value; - options.hasPadValue = true; - } - break; - - case 'S': - parseScrambleSpec(musl_optarg); - break; - - case 't': - options.is32kMode = true; - break; - - // LCOV_EXCL_START - case 'V': - printf("rgblink %s\n", get_package_version_string()); - exit(0); - - case 'v': - incrementVerbosity(); - break; - // LCOV_EXCL_STOP - - case 'W': - warnings.processWarningFlag(musl_optarg); - break; - - case 'w': - options.isWRAM0Mode = true; - break; - - case 'x': - options.disablePadding = true; - // implies tiny mode - options.is32kMode = true; - break; - - case 0: // Long-only options - if (longOpt == 'c' && !style_Parse(musl_optarg)) { - fatal("Invalid argument for option '--color'"); - } - break; - - // LCOV_EXCL_START - default: - usage.printAndExit(1); - // LCOV_EXCL_STOP +static void parseArg(int ch, char *arg) { + switch (ch) { + case 'B': + if (!trace_ParseTraceDepth(arg)) { + fatal("Invalid argument for option '-B'"); } + break; + + case 'd': + options.isDmgMode = true; + options.isWRAM0Mode = true; + break; + + // LCOV_EXCL_START + case 'h': + usage.printAndExit(0); + // LCOV_EXCL_STOP + + case 'l': + if (localOptions.linkerScriptName) { + warnx("Overriding linker script file \"%s\"", localOptions.linkerScriptName->c_str()); + } + localOptions.linkerScriptName = arg; + break; + + case 'M': + options.noSymInMap = true; + break; + + case 'm': + if (options.mapFileName) { + warnx("Overriding map file \"%s\"", options.mapFileName->c_str()); + } + options.mapFileName = arg; + break; + + case 'n': + if (options.symFileName) { + warnx("Overriding sym file \"%s\"", options.symFileName->c_str()); + } + options.symFileName = arg; + break; + + case 'O': + if (options.overlayFileName) { + warnx("Overriding overlay file \"%s\"", options.overlayFileName->c_str()); + } + options.overlayFileName = arg; + break; + + case 'o': + if (options.outputFileName) { + warnx("Overriding output file \"%s\"", options.outputFileName->c_str()); + } + options.outputFileName = arg; + break; + + case 'p': + if (std::optional value = parseWholeNumber(arg); !value) { + fatal("Invalid argument for option '-p'"); + } else if (*value > 0xFF) { + fatal("Argument for option '-p' must be between 0 and 0xFF"); + } else { + options.padValue = *value; + options.hasPadValue = true; + } + break; + + case 'S': + parseScrambleSpec(arg); + break; + + case 't': + options.is32kMode = true; + break; + + // LCOV_EXCL_START + case 'V': + printf("rgblink %s\n", get_package_version_string()); + exit(0); + + case 'v': + incrementVerbosity(); + break; + // LCOV_EXCL_STOP + + case 'W': + warnings.processWarningFlag(arg); + break; + + case 'w': + options.isWRAM0Mode = true; + break; + + case 'x': + options.disablePadding = true; + // implies tiny mode + options.is32kMode = true; + break; + + case 0: // Long-only options + if (longOpt == 'c' && !style_Parse(arg)) { + fatal("Invalid argument for option '--color'"); + } + break; + + case 1: // Positional argument + localOptions.inputFileNames.push_back(arg); + break; + + // LCOV_EXCL_START + default: + usage.printAndExit(1); + // LCOV_EXCL_STOP + } +} + +// LCOV_EXCL_START +static void verboseOutputConfig() { + if (!checkVerbosity(VERB_CONFIG)) { + return; } - verboseOutputConfig(argc, argv); + style_Set(stderr, STYLE_MAGENTA, false); - if (musl_optind == argc) { + fprintf(stderr, "rgblink %s\n", get_package_version_string()); + + printVVVVVVerbosity(); + + fputs("Options:\n", stderr); + // -d/--dmg + if (options.isDmgMode) { + fputs("\tDMG mode prohibits non-DMG section types\n", stderr); + } + // -t/--tiny + if (options.is32kMode) { + fputs("\tROM0 covers the full 32 KiB of ROM\n", stderr); + } + // -w/--wramx + if (options.isWRAM0Mode) { + fputs("\tWRAM0 covers the full 8 KiB of WRAM\n", stderr); + } + // -x/--nopad + if (options.disablePadding) { + fputs("\tNo padding at the end of the ROM file\n", stderr); + } + // -p/--pad + fprintf(stderr, "\tPad value: 0x%02" PRIx8 "\n", options.padValue); + // -S/--scramble + if (options.scrambleROMX || options.scrambleWRAMX || options.scrambleSRAM) { + fputs("\tScramble: ", stderr); + if (options.scrambleROMX) { + fprintf(stderr, "ROMX = %" PRIu16, options.scrambleROMX); + if (options.scrambleWRAMX || options.scrambleSRAM) { + fputs(", ", stderr); + } + } + if (options.scrambleWRAMX) { + fprintf(stderr, "WRAMX = %" PRIu16, options.scrambleWRAMX); + if (options.scrambleSRAM) { + fputs(", ", stderr); + } + } + if (options.scrambleSRAM) { + fprintf(stderr, "SRAM = %" PRIu16, options.scrambleSRAM); + } + putc('\n', stderr); + } + // file ... + if (!localOptions.inputFileNames.empty()) { + fprintf(stderr, "\tInput object files: "); + size_t nbFiles = localOptions.inputFileNames.size(); + for (size_t i = 0; i < nbFiles; ++i) { + if (i > 0) { + fputs(", ", stderr); + } + if (i == 10) { + fprintf(stderr, "and %zu more", nbFiles - i); + break; + } + fputs(localOptions.inputFileNames[i].c_str(), stderr); + } + putc('\n', stderr); + } + auto printPath = [](char const *name, std::optional const &path) { + if (path) { + fprintf(stderr, "\t%s: %s\n", name, path->c_str()); + } + }; + // -O/--overlay + printPath("Overlay file", options.overlayFileName); + // -l/--linkerscript + printPath("Linker script", localOptions.linkerScriptName); + // -o/--output + printPath("Output ROM file", options.outputFileName); + // -m/--map + printPath("Output map file", options.mapFileName); + // -M/--no-sym-in-map + if (options.mapFileName && options.noSymInMap) { + fputs("\tNo symbols in map file\n", stderr); + } + // -n/--sym + printPath("Output sym file", options.symFileName); + fputs("Ready.\n", stderr); + + style_Reset(stderr); +} +// LCOV_EXCL_STOP + +int main(int argc, char *argv[]) { + cli_ParseArgs(argc, argv, optstring, longopts, parseArg, fatal); + + verboseOutputConfig(); + + if (localOptions.inputFileNames.empty()) { usage.printAndExit("No input file specified (pass \"-\" to read from standard input)"); } @@ -427,16 +437,17 @@ int main(int argc, char *argv[]) { } // Read all object files first, - obj_Setup(argc - musl_optind); - for (int i = musl_optind; i < argc; ++i) { - obj_ReadFile(argv[i], argc - i - 1); + size_t nbFiles = localOptions.inputFileNames.size(); + obj_Setup(nbFiles); + for (size_t i = 0; i < nbFiles; ++i) { + obj_ReadFile(localOptions.inputFileNames[i], nbFiles - i - 1); } // apply the linker script's modifications, - if (linkerScriptName) { + if (localOptions.linkerScriptName) { verbosePrint(VERB_NOTICE, "Reading linker script...\n"); - if (lexer_Init(linkerScriptName)) { + if (lexer_Init(*localOptions.linkerScriptName)) { if (yy::parser parser; parser.parse() != 0) { // Exited due to YYABORT or YYNOMEM fatal("Unrecoverable error while reading linker script"); // LCOV_EXCL_LINE diff --git a/src/link/object.cpp b/src/link/object.cpp index 22bee505..0efe3b5f 100644 --- a/src/link/object.cpp +++ b/src/link/object.cpp @@ -405,9 +405,10 @@ static void readAssertion( tryReadString(assert.message, file, "%s: Cannot read assertion's message: %s", fileName); } -void obj_ReadFile(char const *fileName, unsigned int fileID) { +void obj_ReadFile(std::string const &filePath, size_t fileID) { FILE *file; - if (strcmp(fileName, "-")) { + char const *fileName = filePath.c_str(); + if (filePath != "-") { file = fopen(fileName, "rb"); } else { fileName = ""; @@ -553,6 +554,6 @@ void obj_ReadFile(char const *fileName, unsigned int fileID) { } } -void obj_Setup(unsigned int nbFiles) { +void obj_Setup(size_t nbFiles) { nodes.resize(nbFiles); } diff --git a/src/link/output.cpp b/src/link/output.cpp index 9511372f..6de2affb 100644 --- a/src/link/output.cpp +++ b/src/link/output.cpp @@ -208,15 +208,16 @@ static void static void writeROM() { if (options.outputFileName) { - if (strcmp(options.outputFileName, "-")) { - outputFile = fopen(options.outputFileName, "wb"); + char const *outputFileName = options.outputFileName->c_str(); + if (*options.outputFileName != "-") { + outputFile = fopen(outputFileName, "wb"); } else { - options.outputFileName = ""; + outputFileName = ""; (void)setmode(STDOUT_FILENO, O_BINARY); outputFile = stdout; } if (!outputFile) { - fatal("Failed to open output file \"%s\": %s", options.outputFileName, strerror(errno)); + fatal("Failed to open output file \"%s\": %s", outputFileName, strerror(errno)); } } Defer closeOutputFile{[&] { @@ -226,17 +227,16 @@ static void writeROM() { }}; if (options.overlayFileName) { - if (strcmp(options.overlayFileName, "-")) { - overlayFile = fopen(options.overlayFileName, "rb"); + char const *overlayFileName = options.overlayFileName->c_str(); + if (*options.overlayFileName != "-") { + overlayFile = fopen(overlayFileName, "rb"); } else { - options.overlayFileName = ""; + overlayFileName = ""; (void)setmode(STDIN_FILENO, O_BINARY); overlayFile = stdin; } if (!overlayFile) { - fatal( - "Failed to open overlay file \"%s\": %s", options.overlayFileName, strerror(errno) - ); + fatal("Failed to open overlay file \"%s\": %s", overlayFileName, strerror(errno)); } } Defer closeOverlayFile{[&] { @@ -548,15 +548,16 @@ static void writeSym() { return; } - if (strcmp(options.symFileName, "-")) { - symFile = fopen(options.symFileName, "w"); + char const *symFileName = options.symFileName->c_str(); + if (*options.symFileName != "-") { + symFile = fopen(symFileName, "w"); } else { - options.symFileName = ""; + symFileName = ""; (void)setmode(STDOUT_FILENO, O_TEXT); // May have been set to O_BINARY previously symFile = stdout; } if (!symFile) { - fatal("Failed to open sym file \"%s\": %s", options.symFileName, strerror(errno)); + fatal("Failed to open sym file \"%s\": %s", symFileName, strerror(errno)); } Defer closeSymFile{[&] { fclose(symFile); }}; @@ -598,15 +599,16 @@ static void writeMap() { return; } - if (strcmp(options.mapFileName, "-")) { - mapFile = fopen(options.mapFileName, "w"); + char const *mapFileName = options.mapFileName->c_str(); + if (*options.mapFileName != "-") { + mapFile = fopen(mapFileName, "w"); } else { - options.mapFileName = ""; + mapFileName = ""; (void)setmode(STDOUT_FILENO, O_TEXT); // May have been set to O_BINARY previously mapFile = stdout; } if (!mapFile) { - fatal("Failed to open map file \"%s\": %s", options.mapFileName, strerror(errno)); + fatal("Failed to open map file \"%s\": %s", mapFileName, strerror(errno)); } Defer closeMapFile{[&] { fclose(mapFile); }}; diff --git a/test/asm/test.sh b/test/asm/test.sh index 8a4ffcad..891d2c4c 100755 --- a/test/asm/test.sh +++ b/test/asm/test.sh @@ -61,10 +61,9 @@ else fi for i in *.asm notexist.asm; do - flags=${i%.asm}.flags RGBASMFLAGS="-Weverything -Bcollapse" - if [ -f "$flags" ]; then - RGBASMFLAGS="$RGBASMFLAGS $(head -n 1 "$flags")" # Allow other lines to serve as comments + if [ -f "${i%.asm}.flags" ]; then + RGBASMFLAGS="$RGBASMFLAGS @${i%.asm}.flags" fi for variant in '' ' piped'; do (( tests++ )) @@ -134,7 +133,6 @@ for i in *.asm notexist.asm; do done for i in cli/*.flags; do - RGBASMFLAGS="$(head -n 1 "$i")" # Allow other lines to serve as comments (( tests++ )) echo "${bold}${green}${i%.flags}...${rescolors}${resbold}" if [ -e "${i%.flags}.out" ]; then @@ -147,7 +145,7 @@ for i in cli/*.flags; do else desired_errput=/dev/null fi - "$RGBASM" $RGBASMFLAGS >"$output" 2>"$errput" + "$RGBASM" "@$i" >"$output" 2>"$errput" tryDiff "$desired_output" "$output" out our_rc=$? diff --git a/test/fix/disable-warnings.flags b/test/fix/disable-warnings.flags index 78739d7e..a6a04b39 100644 --- a/test/fix/disable-warnings.flags +++ b/test/fix/disable-warnings.flags @@ -1,2 +1,2 @@ -w -m mbc3+ram -r 0 -The "-w" suppresses "-Wmbc" and "-Woverwrite" warnings +# The "-w" suppresses "-Wmbc" and "-Woverwrite" warnings diff --git a/test/fix/dollar-hex.flags b/test/fix/dollar-hex.flags index 40f14473..321e29a4 100644 --- a/test/fix/dollar-hex.flags +++ b/test/fix/dollar-hex.flags @@ -1 +1 @@ --m '$2a' +-m $2a diff --git a/test/fix/gameid-trunc.flags b/test/fix/gameid-trunc.flags index 1ccf56c6..02720832 100644 --- a/test/fix/gameid-trunc.flags +++ b/test/fix/gameid-trunc.flags @@ -1 +1 @@ --i 'FOUR!' +-i FOUR! diff --git a/test/fix/header-edit.flags b/test/fix/header-edit.flags index fc6cb560..cfabe62c 100644 --- a/test/fix/header-edit.flags +++ b/test/fix/header-edit.flags @@ -1,2 +1,2 @@ -Cf h -Checks that the header checksum properly accounts for header modifications +# Checks that the header checksum properly accounts for header modifications diff --git a/test/fix/overwrite.flags b/test/fix/overwrite.flags index fadc88b8..4713ddb1 100644 --- a/test/fix/overwrite.flags +++ b/test/fix/overwrite.flags @@ -1,2 +1,2 @@ -Wno-overwrite -Cjv -t PM_CRYSTAL -i BYTE -n 0 -k 01 -l 0x33 -m 0x10 -r 3 -p 0 -Checks that the -Wno-overwrite flag suppresses "Overwrote a non-zero byte" warnings from the rest +# Checks that the -Wno-overwrite flag suppresses "Overwrote a non-zero byte" warnings from the rest diff --git a/test/fix/test.sh b/test/fix/test.sh index 550a5beb..f69ea988 100755 --- a/test/fix/test.sh +++ b/test/fix/test.sh @@ -50,10 +50,14 @@ tryCmp () { } runTest () { - flags=$( - head -n 1 "$2/$1.flags" | # Allow other lines to serve as comments - sed "s# ./# ${src//#/\\#}/#g" # Prepend src directory to path arguments - ) + if grep -qF ' ./' "$2/$1.flags"; then + flags=$( + head -n 1 "$2/$1.flags" | # Allow other lines to serve as comments + sed "s# ./# ${src//#/\\#}/#g" # Prepend src directory to path arguments + ) + else + flags="@$2/$1.flags" + fi for variant in '' ' piped' ' output'; do (( tests++ )) diff --git a/test/fix/title-color-trunc-rev.flags b/test/fix/title-color-trunc-rev.flags index 9df669f0..11bee699 100644 --- a/test/fix/title-color-trunc-rev.flags +++ b/test/fix/title-color-trunc-rev.flags @@ -1,3 +1,3 @@ -t 0123456789ABCDEF -C -Checks that the CGB flag correctly truncates the title to 15 chars only, -even when it's specified *after* the title..! +# Checks that the CGB flag correctly truncates the title to 15 chars only, +# even when it's specified *after* the title..! diff --git a/test/fix/title-color-trunc.flags b/test/fix/title-color-trunc.flags index 141ecbfd..5d17369e 100644 --- a/test/fix/title-color-trunc.flags +++ b/test/fix/title-color-trunc.flags @@ -1,2 +1,2 @@ -C -t 0123456789ABCDEF -Checks that the CGB flag correctly truncates the title to 15 chars only +# Checks that the CGB flag correctly truncates the title to 15 chars only diff --git a/test/fix/title-compat-trunc-rev.flags b/test/fix/title-compat-trunc-rev.flags index dd3c4fca..64c51e1f 100644 --- a/test/fix/title-compat-trunc-rev.flags +++ b/test/fix/title-compat-trunc-rev.flags @@ -1,3 +1,3 @@ -t 0123456789ABCDEF -c -Checks that the CGB compat flag correctly truncates the title to 15 chars only, -even when it's specified *after* the title..! +# Checks that the CGB compat flag correctly truncates the title to 15 chars only, +# even when it's specified *after* the title..! diff --git a/test/fix/title-compat-trunc.flags b/test/fix/title-compat-trunc.flags index b0f3a762..388bf0f7 100644 --- a/test/fix/title-compat-trunc.flags +++ b/test/fix/title-compat-trunc.flags @@ -1,2 +1,2 @@ -c -t 0123456789ABCDEF -Checks that the CGB compat flag correctly truncates the title to 15 chars only +# Checks that the CGB compat flag correctly truncates the title to 15 chars only diff --git a/test/fix/title-gameid-trunc-rev.flags b/test/fix/title-gameid-trunc-rev.flags index 4ed79f35..7e9591ca 100644 --- a/test/fix/title-gameid-trunc-rev.flags +++ b/test/fix/title-gameid-trunc-rev.flags @@ -1,3 +1,3 @@ -t 0123456789ABCDEF -i rgbd -Checks that the game ID flag correctly truncates the title to 11 chars only, -even when it's specified *after* the title..! +# Checks that the game ID flag correctly truncates the title to 11 chars only, +# even when it's specified *after* the title..! diff --git a/test/fix/title-gameid-trunc.flags b/test/fix/title-gameid-trunc.flags index c6273db4..474ed1eb 100644 --- a/test/fix/title-gameid-trunc.flags +++ b/test/fix/title-gameid-trunc.flags @@ -1,2 +1,2 @@ -i rgbd -t 0123456789ABCDEF -Checks that the game ID flag correctly truncates the title to 11 chars only +# Checks that the game ID flag correctly truncates the title to 11 chars only diff --git a/test/fix/title-pad.flags b/test/fix/title-pad.flags index f156c554..459246fa 100644 --- a/test/fix/title-pad.flags +++ b/test/fix/title-pad.flags @@ -1 +1 @@ --t "I LOVE YOU" +-t I_LOVE_YOU diff --git a/test/fix/title-pad.gb b/test/fix/title-pad.gb index 3b5cb293a88d23c20427501bad12823c20ad13f6..3514f0ca3af1786336df28c3a2d715ac5d545eb8 100644 GIT binary patch delta 18 ZcmZo*X<*r4%E%V);~(Z4KiQh`C;%{`1$_Vj delta 18 ZcmZo*X<*r4%E+eR;~(a#Fxi^%C;%++1pfd4 diff --git a/test/fix/title.flags b/test/fix/title.flags index 3d8cc99a..cbcaa590 100644 --- a/test/fix/title.flags +++ b/test/fix/title.flags @@ -1 +1 @@ --t "Game Boy dev rox" +-t Game_Boy_dev_rox diff --git a/test/fix/title.gb b/test/fix/title.gb index 614591b10fe9e155f06fac097289a901f54cc0bf..78777693ee39c948135c2f8bcee7de8a7b807f0e 100644 GIT binary patch delta 21 ccmZo*X<*r4!N?i!lwTR2l3Er&*^zNS07rcX6#xJL delta 21 ccmZo*X<*r4!N{rLlwYZkl3J!P*^zNS06^FVSO5S3 diff --git a/test/fix/unknown-mbc.flags b/test/fix/unknown-mbc.flags index c2c83ddf..7d301d9a 100644 --- a/test/fix/unknown-mbc.flags +++ b/test/fix/unknown-mbc.flags @@ -1,2 +1 @@ -m MBC1337 - diff --git a/test/fix/verify-pad.flags b/test/fix/verify-pad.flags index aa139d70..08a811c7 100644 --- a/test/fix/verify-pad.flags +++ b/test/fix/verify-pad.flags @@ -1,4 +1,4 @@ -vp 69 -Check that the global checksum is correctly affected by padding: -Padding adds extra bytes (carefully picked *not* to be 0, or other values), -which must be properly accounted for. +# Check that the global checksum is correctly affected by padding: +# Padding adds extra bytes (carefully picked *not* to be 0, or other values), +# which must be properly accounted for. diff --git a/test/fix/verify-trash.flags b/test/fix/verify-trash.flags index a9b4f7c1..2ca7bb08 100644 --- a/test/fix/verify-trash.flags +++ b/test/fix/verify-trash.flags @@ -1,2 +1,2 @@ -f LHG -Checks that the global checksum is correctly affected by the header checksum +# Checks that the global checksum is correctly affected by the header checksum diff --git a/test/fix/verify.flags b/test/fix/verify.flags index a9105b8d..655ef745 100644 --- a/test/fix/verify.flags +++ b/test/fix/verify.flags @@ -1,2 +1,2 @@ -v -Checks that the global checksum is correctly affected by the header checksum +# Checks that the global checksum is correctly affected by the header checksum