Enable RGBGFX's CLI "at-files" for all programs (#1848)

This commit is contained in:
Rangi
2025-10-22 17:05:59 -04:00
committed by GitHub
parent a0bb830679
commit f065243cd2
44 changed files with 1466 additions and 1334 deletions

View File

@@ -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`:**

View File

@@ -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 \

View File

@@ -3,6 +3,7 @@
#ifndef RGBDS_ASM_MAIN_HPP
#define RGBDS_ASM_MAIN_HPP
#include <optional>
#include <stdint.h>
#include <stdio.h>
#include <string>
@@ -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<std::string> targetFileName{}; // -MQ, -MT
MissingInclude missingIncludeState = INC_ERROR; // -MC, -MG
bool generatePhonyDeps = false; // -MP
std::string objectFileName; // -o
std::optional<std::string> 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());
}
}
};

20
include/cli.hpp Normal file
View File

@@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT
#ifndef RGBDS_CLI_HPP
#define RGBDS_CLI_HPP
#include <stdarg.h>
#include <string>
#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

View File

@@ -3,7 +3,9 @@
#ifndef RGBDS_FIX_MAIN_HPP
#define RGBDS_FIX_MAIN_HPP
#include <optional>
#include <stdint.h>
#include <string>
#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<std::string> gameID; // -i
uint8_t gameIDLen;
char const *newLicensee = nullptr; // -k
std::optional<std::string> newLicensee; // -k
uint8_t newLicenseeLen;
char const *logoFilename = nullptr; // -L
std::optional<std::string> logoFilename; // -L
uint8_t logo[48] = {};
MbcType cartridgeType = MBC_NONE; // -m
uint8_t tpp1Rev[2];
char const *title = nullptr; // -t
std::optional<std::string> title; // -t
uint8_t titleLen;
};

View File

@@ -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

View File

@@ -3,15 +3,17 @@
#ifndef RGBDS_LINK_MAIN_HPP
#define RGBDS_LINK_MAIN_HPP
#include <optional>
#include <stdint.h>
#include <string>
struct Options {
bool isDmgMode; // -d
char const *mapFileName; // -m
std::optional<std::string> mapFileName; // -m
bool noSymInMap; // -M
char const *symFileName; // -n
char const *overlayFileName; // -O
char const *outputFileName; // -o
std::optional<std::string> symFileName; // -n
std::optional<std::string> overlayFileName; // -O
std::optional<std::string> outputFileName; // -o
uint8_t padValue; // -p
bool hasPadValue = false;
// Setting these three to 0 disables the functionality

View File

@@ -3,10 +3,13 @@
#ifndef RGBDS_LINK_OBJECT_HPP
#define RGBDS_LINK_OBJECT_HPP
#include <stddef.h>
#include <string>
// 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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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"

View File

@@ -19,8 +19,8 @@
#include <vector>
#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<std::string, std::vector<StateFeature>> stateFileSpecs; // -s
// Flags which must be processed after the option parsing finishes
static struct LocalOptions {
std::optional<std::string> dependFileName; // -M
std::unordered_map<std::string, std::vector<StateFeature>> stateFileSpecs; // -s
std::optional<std::string> inputFileName; // <file>
} 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 == "-" ? "<stdout>" : 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 : "<stdout>"
);
// -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,46 +171,29 @@ static std::vector<StateFeature> parseStateFeatures(char *str) {
return features;
}
int main(int argc, char *argv[]) {
// Support SOURCE_DATE_EPOCH for reproducible builds
// https://reproducible-builds.org/docs/source-date-epoch/
time_t now = time(nullptr);
if (char const *sourceDateEpoch = getenv("SOURCE_DATE_EPOCH"); sourceDateEpoch) {
// Use `strtoul`, not `parseWholeNumber`, because SOURCE_DATE_EPOCH does
// not conventionally support our custom base prefixes
now = static_cast<time_t>(strtoul(sourceDateEpoch, nullptr, 0));
}
sym_Init(now);
// Maximum of 100 errors only applies if rgbasm is printing errors to a terminal
if (isatty(STDERR_FILENO)) {
options.maxErrors = 100; // LCOV_EXCL_LINE
}
// Parse CLI options
for (int ch; (ch = musl_getopt_long_only(argc, argv, optstring, longopts, nullptr)) != -1;) {
static void parseArg(int ch, char *arg) {
switch (ch) {
case 'B':
if (!trace_ParseTraceDepth(musl_optarg)) {
if (!trace_ParseTraceDepth(arg)) {
fatal("Invalid argument for option '-B'");
}
break;
case 'b':
if (strlen(musl_optarg) == 2) {
opt_B(musl_optarg);
if (strlen(arg) == 2) {
opt_B(arg);
} else {
fatal("Must specify exactly 2 characters for option '-b'");
}
break;
case 'D': {
char *equals = strchr(musl_optarg, '=');
char *equals = strchr(arg, '=');
if (equals) {
*equals = '\0';
sym_AddString(musl_optarg, std::make_shared<std::string>(equals + 1));
sym_AddString(arg, std::make_shared<std::string>(equals + 1));
} else {
sym_AddString(musl_optarg, std::make_shared<std::string>("1"));
sym_AddString(arg, std::make_shared<std::string>("1"));
}
break;
}
@@ -344,8 +203,8 @@ int main(int argc, char *argv[]) {
break;
case 'g':
if (strlen(musl_optarg) == 4) {
opt_G(musl_optarg);
if (strlen(arg) == 4) {
opt_G(arg);
} else {
fatal("Must specify exactly 4 characters for option '-g'");
}
@@ -357,32 +216,33 @@ int main(int argc, char *argv[]) {
// LCOV_EXCL_STOP
case 'I':
fstk_AddIncludePath(musl_optarg);
fstk_AddIncludePath(arg);
break;
case 'M':
if (dependFileName) {
if (localOptions.dependFileName) {
warnx(
"Overriding dependency file \"%s\"",
strcmp(dependFileName, "-") ? dependFileName : "<stdout>"
*localOptions.dependFileName == "-" ? "<stdout>"
: localOptions.dependFileName->c_str()
);
}
dependFileName = musl_optarg;
localOptions.dependFileName = arg;
break;
case 'o':
if (!options.objectFileName.empty()) {
warnx("Overriding output file \"%s\"", options.objectFileName.c_str());
if (options.objectFileName) {
warnx("Overriding output file \"%s\"", options.objectFileName->c_str());
}
options.objectFileName = musl_optarg;
options.objectFileName = arg;
break;
case 'P':
fstk_AddPreIncludeFile(musl_optarg);
fstk_AddPreIncludeFile(arg);
break;
case 'p':
if (std::optional<uint64_t> padByte = parseWholeNumber(musl_optarg); !padByte) {
if (std::optional<uint64_t> 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");
@@ -392,7 +252,7 @@ int main(int argc, char *argv[]) {
break;
case 'Q': {
char const *precisionArg = musl_optarg;
char const *precisionArg = arg;
if (precisionArg[0] == '.') {
++precisionArg;
}
@@ -408,7 +268,7 @@ int main(int argc, char *argv[]) {
}
case 'r':
if (std::optional<uint64_t> maxDepth = parseWholeNumber(musl_optarg); !maxDepth) {
if (std::optional<uint64_t> maxDepth = parseWholeNumber(arg); !maxDepth) {
fatal("Invalid argument for option '-r'");
} else if (errno == ERANGE) {
fatal("Argument for option '-r' is out of range");
@@ -418,19 +278,19 @@ int main(int argc, char *argv[]) {
break;
case 's': {
// Split "<features>:<name>" so `musl_optarg` is "<features>" and `name` is "<name>"
char *name = strchr(musl_optarg, ':');
// Split "<features>:<name>" so `arg` is "<features>" and `name` is "<name>"
char *name = strchr(arg, ':');
if (!name) {
fatal("Invalid argument for option '-s'");
}
*name++ = '\0';
std::vector<StateFeature> features = parseStateFeatures(musl_optarg);
std::vector<StateFeature> features = parseStateFeatures(arg);
if (stateFileSpecs.find(name) != stateFileSpecs.end()) {
if (localOptions.stateFileSpecs.find(name) != localOptions.stateFileSpecs.end()) {
warnx("Overriding state file \"%s\"", name);
}
stateFileSpecs.emplace(name, std::move(features));
localOptions.stateFileSpecs.emplace(name, std::move(features));
break;
}
@@ -445,7 +305,7 @@ int main(int argc, char *argv[]) {
// LCOV_EXCL_STOP
case 'W':
opt_W(musl_optarg);
opt_W(arg);
break;
case 'w':
@@ -453,7 +313,7 @@ int main(int argc, char *argv[]) {
break;
case 'X':
if (std::optional<uint64_t> maxErrors = parseWholeNumber(musl_optarg); !maxErrors) {
if (std::optional<uint64_t> 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);
@@ -465,7 +325,7 @@ int main(int argc, char *argv[]) {
case 0: // Long-only options
switch (longOpt) {
case 'c':
if (!style_Parse(musl_optarg)) {
if (!style_Parse(arg)) {
fatal("Invalid argument for option '--color'");
}
break;
@@ -484,19 +344,28 @@ int main(int argc, char *argv[]) {
case 'Q':
case 'T': {
std::string newTarget = musl_optarg;
std::string newTarget = arg;
if (longOpt == 'Q') {
newTarget = escapeMakeChars(newTarget);
}
if (!options.targetFileName.empty()) {
options.targetFileName += ' ';
if (options.targetFileName) {
*options.targetFileName += ' ';
*options.targetFileName += newTarget;
} else {
options.targetFileName = newTarget;
}
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);
@@ -504,46 +373,194 @@ int main(int argc, char *argv[]) {
}
}
if (options.targetFileName.empty() && !options.objectFileName.empty()) {
// 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 == "-" ? "<stdout>" : 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 == "-" ? "<stdin>" : 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/
time_t now = time(nullptr);
if (char const *sourceDateEpoch = getenv("SOURCE_DATE_EPOCH"); sourceDateEpoch) {
// Use `strtoul`, not `parseWholeNumber`, because SOURCE_DATE_EPOCH does
// not conventionally support our custom base prefixes
now = static_cast<time_t>(strtoul(sourceDateEpoch, nullptr, 0));
}
sym_Init(now);
// Maximum of 100 errors only applies if rgbasm is printing errors to a terminal
if (isatty(STDERR_FILENO)) {
options.maxErrors = 100; // LCOV_EXCL_LINE
}
cli_ParseArgs(argc, argv, optstring, longopts, parseArg, fatal);
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 == "-" ? "<stdin>" : 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);
}

View File

@@ -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 = "<stdout>";
objectFileName = "<stdout>";
(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); }};

153
src/cli.cpp Normal file
View File

@@ -0,0 +1,153 @@
#include "cli.hpp"
#include <errno.h>
#include <fstream>
#include <string.h>
#include <string>
#include <vector>
#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<size_t> readAtFile(
std::string const &path, std::vector<char> &argPool, void (*fatal)(char const *, ...)
) {
std::vector<size_t> 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<char *> argv; // This context's arg pointer vec
AtFileStackEntry(int parentInd_, std::vector<char *> argv_)
: parentInd(parentInd_), argv(argv_) {}
};
std::vector<AtFileStackEntry> atFileStack;
int curArgc = argc;
char **curArgv = argv;
std::string optString = "-"s + shortOpts; // Request position arguments with a leading '-'
std::vector<std::vector<char>> 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<char> &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<size_t> 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<char *> &vec = atFileStack.back().argv;
curArgc = vec.size();
curArgv = vec.data();
}
}
}
}

View File

@@ -141,7 +141,11 @@ static void
if (options.title) {
overwriteBytes(
rom0, 0x134, reinterpret_cast<uint8_t const *>(options.title), options.titleLen, "title"
rom0,
0x134,
reinterpret_cast<uint8_t const *>(options.title->c_str()),
options.titleLen,
"title"
);
}
@@ -149,7 +153,7 @@ static void
overwriteBytes(
rom0,
0x13F,
reinterpret_cast<uint8_t const *>(options.gameID),
reinterpret_cast<uint8_t const *>(options.gameID->c_str()),
options.gameIDLen,
"manufacturer code"
);
@@ -163,7 +167,7 @@ static void
overwriteBytes(
rom0,
0x144,
reinterpret_cast<uint8_t const *>(options.newLicensee),
reinterpret_cast<uint8_t const *>(options.newLicensee->c_str()),
options.newLicenseeLen,
"new licensee code"
);

View File

@@ -12,8 +12,8 @@
#include <stdlib.h>
#include <string.h>
#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<std::string> outputFileName; // -o
std::vector<std::string> inputFileNames; // <file>...
} 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<uint64_t> value = parseWholeNumber(musl_optarg); !value) {
static uint16_t parseByte(char const *input, char name) {
if (std::optional<uint64_t> 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 = "<stdin>";
logoFilename = "<stdin>";
(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;
}

View File

@@ -2,7 +2,6 @@
#include "gfx/main.hpp"
#include <errno.h>
#include <inttypes.h>
#include <ios>
#include <optional>
@@ -11,11 +10,12 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <string_view>
#include <vector>
#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<std::string> 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,75 +138,8 @@ 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;
}
}
// Turn an at-file's contents into an argv that `getopt` can handle, appending them to `argPool`.
static std::vector<size_t> readAtFile(std::string const &path, std::vector<char> &argPool) {
File file;
if (!file.open(path, std::ios_base::in)) {
fatal("Error reading at-file \"%s\": %s", file.c_str(path), strerror(errno));
}
for (std::vector<size_t> argvOfs;;) {
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
}
}
// 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
static void parseArg(int ch, char *arg) {
char const *argPtr = arg; // Make a copy for scanning
switch (ch) {
case 'A':
@@ -217,45 +151,39 @@ static char *parseArgv(int argc, char *argv[]) {
if (!options.attrmap.empty()) {
warnx("Overriding attrmap file \"%s\"", options.attrmap.c_str());
}
options.attrmap = musl_optarg;
options.attrmap = arg;
break;
case 'B':
parseBackgroundPalSpec(musl_optarg);
parseBackgroundPalSpec(arg);
break;
case 'b': {
uint16_t number = readNumber(arg, "Bank 0 base tile ID", 0);
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 (*arg == '\0') {
if (*argPtr == '\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
);
skipBlankSpace(argPtr);
if (*argPtr != ',') {
error("Base tile IDs must be one or two comma-separated numbers, not \"%s\"", arg);
break;
}
++arg; // Skip comma
skipBlankSpace(arg);
number = readNumber(arg, "Bank 1 base tile ID", 0);
++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 (*arg != '\0') {
error(
"Base tile IDs must be one or two comma-separated numbers, not \"%s\"",
musl_optarg
);
if (*argPtr != '\0') {
error("Base tile IDs must be one or two comma-separated numbers, not \"%s\"", arg);
break;
}
break;
@@ -266,31 +194,31 @@ static char *parseArgv(int argc, char *argv[]) {
break;
case 'c':
localOptions.externalPalSpec = nullptr; // Allow overriding a previous pal spec
if (musl_optarg[0] == '#') {
localOptions.externalPalSpec = std::nullopt; // Allow overriding a previous pal spec
if (arg[0] == '#') {
options.palSpecType = Options::EXPLICIT;
parseInlinePalSpec(musl_optarg);
} else if (strcasecmp(musl_optarg, "embedded") == 0) {
parseInlinePalSpec(arg);
} else if (strcasecmp(arg, "embedded") == 0) {
// Use PLTE, error out if missing
options.palSpecType = Options::EMBEDDED;
} else if (strcasecmp(musl_optarg, "auto") == 0) {
} else if (strcasecmp(arg, "auto") == 0) {
options.palSpecType = Options::NO_SPEC;
} else if (strcasecmp(musl_optarg, "dmg") == 0) {
} else if (strcasecmp(arg, "dmg") == 0) {
options.palSpecType = Options::DMG;
parseDmgPalSpec(0xE4); // Same darkest-first order as `sortGrayscale`
} else if (strncasecmp(musl_optarg, "dmg=", literal_strlen("dmg=")) == 0) {
} else if (strncasecmp(arg, "dmg=", literal_strlen("dmg=")) == 0) {
options.palSpecType = Options::DMG;
parseDmgPalSpec(&musl_optarg[literal_strlen("dmg=")]);
parseDmgPalSpec(&arg[literal_strlen("dmg=")]);
} else {
options.palSpecType = Options::EXPLICIT;
localOptions.externalPalSpec = musl_optarg;
localOptions.externalPalSpec = arg;
}
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);
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;
@@ -306,54 +234,54 @@ static char *parseArgv(int argc, char *argv[]) {
if (!options.inputTileset.empty()) {
warnx("Overriding input tileset file \"%s\"", options.inputTileset.c_str());
}
options.inputTileset = musl_optarg;
options.inputTileset = arg;
break;
case 'L':
options.inputSlice.left = readNumber(arg, "Input slice left coordinate");
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;
}
skipBlankSpace(arg);
if (*arg != ',') {
error("Missing comma after left coordinate in \"%s\"", musl_optarg);
skipBlankSpace(argPtr);
if (*argPtr != ',') {
error("Missing comma after left coordinate in \"%s\"", arg);
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);
++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;
}
++arg;
skipBlankSpace(arg);
options.inputSlice.width = readNumber(arg, "Input slice width");
skipBlankSpace(arg);
++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 (*arg != ',') {
error("Missing comma after width in \"%s\"", musl_optarg);
if (*argPtr != ',') {
error("Missing comma after width in \"%s\"", arg);
break;
}
++arg;
skipBlankSpace(arg);
options.inputSlice.height = readNumber(arg, "Input slice height");
++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 (*arg != '\0') {
error("Unexpected extra characters after slice spec in \"%s\"", musl_optarg);
if (*argPtr != '\0') {
error("Unexpected extra characters after slice spec in \"%s\"", arg);
}
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);
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 {
@@ -372,41 +300,35 @@ static char *parseArgv(int argc, char *argv[]) {
break;
case 'N':
options.maxNbTiles[0] = readNumber(arg, "Number of tiles in bank 0", 256);
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 (*arg == '\0') {
if (*argPtr == '\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
);
skipBlankSpace(argPtr);
if (*argPtr != ',') {
error("Bank capacity must be one or two comma-separated numbers, not \"%s\"", arg);
break;
}
++arg; // Skip comma
skipBlankSpace(arg);
options.maxNbTiles[1] = readNumber(arg, "Number of tiles in bank 1", 256);
++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 (*arg != '\0') {
error(
"Bank capacity must be one or two comma-separated numbers, not \"%s\"",
musl_optarg
);
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(arg, "Number of palettes", 256);
if (*arg != '\0') {
error("Number of palettes ('-n') must be a valid number, not \"%s\"", musl_optarg);
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!");
@@ -426,7 +348,7 @@ static char *parseArgv(int argc, char *argv[]) {
if (!options.output.empty()) {
warnx("Overriding tile data file %s", options.output.c_str());
}
options.output = musl_optarg;
options.output = arg;
break;
case 'P':
@@ -438,7 +360,7 @@ static char *parseArgv(int argc, char *argv[]) {
if (!options.palettes.empty()) {
warnx("Overriding palettes file %s", options.palettes.c_str());
}
options.palettes = musl_optarg;
options.palettes = arg;
break;
case 'Q':
@@ -450,23 +372,21 @@ static char *parseArgv(int argc, char *argv[]) {
if (!options.palmap.empty()) {
warnx("Overriding palette map file %s", options.palmap.c_str());
}
options.palmap = musl_optarg;
options.palmap = arg;
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
);
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(arg, "Number of colors per palette", 4);
if (*arg != '\0') {
error("Palette size ('-s') must be a valid number, not \"%s\"", musl_optarg);
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!");
@@ -484,7 +404,7 @@ static char *parseArgv(int argc, char *argv[]) {
if (!options.tilemap.empty()) {
warnx("Overriding tilemap file %s", options.tilemap.c_str());
}
options.tilemap = musl_optarg;
options.tilemap = arg;
break;
// LCOV_EXCL_START
@@ -498,7 +418,7 @@ static char *parseArgv(int argc, char *argv[]) {
// LCOV_EXCL_STOP
case 'W':
warnings.processWarningFlag(musl_optarg);
warnings.processWarningFlag(arg);
break;
case 'w':
@@ -506,9 +426,9 @@ static char *parseArgv(int argc, char *argv[]) {
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);
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;
@@ -527,17 +447,22 @@ static char *parseArgv(int argc, char *argv[]) {
break;
case 0: // Long-only options
if (longOpt == 'c' && !style_Parse(musl_optarg)) {
if (longOpt == 'c' && !style_Parse(arg)) {
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];
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 {
registerInput(musl_optarg);
options.input = arg;
}
break;
@@ -548,9 +473,6 @@ static char *parseArgv(int argc, char *argv[]) {
}
}
return nullptr; // Done processing this argv
}
// LCOV_EXCL_START
static void verboseOutputConfig() {
if (!checkVerbosity(VERB_CONFIG)) {
@@ -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<char *> argv; // This context's arg pointer vec
AtFileStackEntry(int parentInd_, std::vector<char *> argv_)
: parentInd(parentInd_), argv(argv_) {}
};
std::vector<AtFileStackEntry> atFileStack;
// Parse CLI options
int curArgc = argc;
char **curArgv = argv;
std::vector<std::vector<char>> 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<char> &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<size_t> 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<char *> &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

View File

@@ -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;
}

View File

@@ -13,8 +13,8 @@
#include <utility>
#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<std::string> linkerScriptName; // -l
std::vector<std::string> inputFileNames; // <file>...
} 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,12 +205,10 @@ 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;) {
static void parseArg(int ch, char *arg) {
switch (ch) {
case 'B':
if (!trace_ParseTraceDepth(musl_optarg)) {
if (!trace_ParseTraceDepth(arg)) {
fatal("Invalid argument for option '-B'");
}
break;
@@ -313,10 +224,10 @@ int main(int argc, char *argv[]) {
// LCOV_EXCL_STOP
case 'l':
if (linkerScriptName) {
warnx("Overriding linker script file \"%s\"", linkerScriptName);
if (localOptions.linkerScriptName) {
warnx("Overriding linker script file \"%s\"", localOptions.linkerScriptName->c_str());
}
linkerScriptName = musl_optarg;
localOptions.linkerScriptName = arg;
break;
case 'M':
@@ -325,34 +236,34 @@ int main(int argc, char *argv[]) {
case 'm':
if (options.mapFileName) {
warnx("Overriding map file \"%s\"", options.mapFileName);
warnx("Overriding map file \"%s\"", options.mapFileName->c_str());
}
options.mapFileName = musl_optarg;
options.mapFileName = arg;
break;
case 'n':
if (options.symFileName) {
warnx("Overriding sym file \"%s\"", options.symFileName);
warnx("Overriding sym file \"%s\"", options.symFileName->c_str());
}
options.symFileName = musl_optarg;
options.symFileName = arg;
break;
case 'O':
if (options.overlayFileName) {
warnx("Overriding overlay file \"%s\"", options.overlayFileName);
warnx("Overriding overlay file \"%s\"", options.overlayFileName->c_str());
}
options.overlayFileName = musl_optarg;
options.overlayFileName = arg;
break;
case 'o':
if (options.outputFileName) {
warnx("Overriding output file \"%s\"", options.outputFileName);
warnx("Overriding output file \"%s\"", options.outputFileName->c_str());
}
options.outputFileName = musl_optarg;
options.outputFileName = arg;
break;
case 'p':
if (std::optional<uint64_t> value = parseWholeNumber(musl_optarg); !value) {
if (std::optional<uint64_t> 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");
@@ -363,7 +274,7 @@ int main(int argc, char *argv[]) {
break;
case 'S':
parseScrambleSpec(musl_optarg);
parseScrambleSpec(arg);
break;
case 't':
@@ -381,7 +292,7 @@ int main(int argc, char *argv[]) {
// LCOV_EXCL_STOP
case 'W':
warnings.processWarningFlag(musl_optarg);
warnings.processWarningFlag(arg);
break;
case 'w':
@@ -395,11 +306,15 @@ int main(int argc, char *argv[]) {
break;
case 0: // Long-only options
if (longOpt == 'c' && !style_Parse(musl_optarg)) {
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);
@@ -407,9 +322,104 @@ int main(int argc, char *argv[]) {
}
}
verboseOutputConfig(argc, argv);
// LCOV_EXCL_START
static void verboseOutputConfig() {
if (!checkVerbosity(VERB_CONFIG)) {
return;
}
if (musl_optind == argc) {
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 (!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<std::string> 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

View File

@@ -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 = "<stdin>";
@@ -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);
}

View File

@@ -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 = "<stdout>";
outputFileName = "<stdout>";
(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 = "<stdin>";
overlayFileName = "<stdin>";
(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 = "<stdout>";
symFileName = "<stdout>";
(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 = "<stdout>";
mapFileName = "<stdout>";
(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); }};

View File

@@ -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=$?

View File

@@ -1,2 +1,2 @@
-w -m mbc3+ram -r 0
The "-w" suppresses "-Wmbc" and "-Woverwrite" warnings
# The "-w" suppresses "-Wmbc" and "-Woverwrite" warnings

View File

@@ -1 +1 @@
-m '$2a'
-m $2a

View File

@@ -1 +1 @@
-i 'FOUR!'
-i FOUR!

View File

@@ -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

View File

@@ -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

View File

@@ -50,10 +50,14 @@ tryCmp () {
}
runTest () {
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++ ))

View File

@@ -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..!

View File

@@ -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

View File

@@ -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..!

View File

@@ -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

View File

@@ -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..!

View File

@@ -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

View File

@@ -1 +1 @@
-t "I LOVE YOU"
-t I_LOVE_YOU

Binary file not shown.

View File

@@ -1 +1 @@
-t "Game Boy dev rox"
-t Game_Boy_dev_rox

Binary file not shown.

View File

@@ -1,2 +1 @@
-m MBC1337

View File

@@ -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.

View File

@@ -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

View File

@@ -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