diff --git a/src/cli.zig b/src/cli.zig index 007b482..f699929 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -59,6 +59,14 @@ pub const usage_string_after_command_name = \\ the .rc includes or otherwise depends on. \\ /:depfile-fmt Output format of the depfile, if /:depfile is set. \\ json (default) A top-level JSON array of paths + \\ /:input-format If not specified, the input format is inferred. + \\ rc (default if input format cannot be inferred) + \\ res Compiled .rc file, implies /:output-format coff + \\ rcpp Preprocessed .rc file, implies /:no-preprocess + \\ /:output-format If not specified, the output format is inferred. + \\ res (default if output format cannot be inferred) + \\ coff COFF object file (extension: .obj or .o) + \\ rcpp Preprocessed .rc file, implies /p \\ \\Note: For compatibility reasons, all custom options start with : \\ @@ -149,9 +157,25 @@ pub const Options = struct { auto_includes: AutoIncludes = .any, depfile_path: ?[]const u8 = null, depfile_fmt: DepfileFormat = .json, + input_format: InputFormat = .rc, + output_format: OutputFormat = .res, pub const AutoIncludes = enum { any, msvc, gnu, none }; pub const DepfileFormat = enum { json }; + pub const InputFormat = enum { rc, res, rcpp }; + pub const OutputFormat = enum { + res, + coff, + rcpp, + + pub fn extension(format: OutputFormat) []const u8 { + return switch (format) { + .rcpp => ".rcpp", + .coff => ".obj", + .res => ".res", + }; + } + }; pub const Preprocess = enum { no, yes, only }; pub const SymbolAction = enum { define, undefine }; pub const SymbolValue = union(SymbolAction) { @@ -246,8 +270,8 @@ pub const Options = struct { } pub fn dumpVerbose(self: *const Options, writer: anytype) !void { - try writer.print("Input filename: {s}\n", .{self.input_filename}); - try writer.print("Output filename: {s}\n", .{self.output_filename}); + try writer.print("Input filename: {s} (format={s})\n", .{ self.input_filename, @tagName(self.input_format) }); + try writer.print("Output filename: {s} (format={s})\n", .{ self.output_filename, @tagName(self.output_format) }); if (self.extra_include_paths.items.len > 0) { try writer.writeAll(" Extra include paths:\n"); for (self.extra_include_paths.items) |extra_include_path| { @@ -391,6 +415,7 @@ pub const Arg = struct { pub const Context = struct { index: usize, + option_len: usize, arg: Arg, value: Value, }; @@ -405,7 +430,18 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn errdefer options.deinit(); var output_filename: ?[]const u8 = null; - var output_filename_context: Arg.Context = undefined; + var output_filename_context: union(enum) { + unspecified: void, + positional: usize, + arg: Arg.Context, + } = .{ .unspecified = {} }; + var output_format: ?Options.OutputFormat = null; + var output_format_context: Arg.Context = undefined; + var input_format: ?Options.InputFormat = null; + var input_format_context: Arg.Context = undefined; + var input_filename_arg_i: usize = undefined; + var preprocess_only_context: Arg.Context = undefined; + var depfile_context: Arg.Context = undefined; var arg_i: usize = 0; next_arg: while (arg_i < args.len) { @@ -437,6 +473,25 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn if (std.ascii.startsWithIgnoreCase(arg_name, ":no-preprocess")) { options.preprocess = .no; arg.name_offset += ":no-preprocess".len; + } else if (std.ascii.startsWithIgnoreCase(arg_name, ":output-format")) { + const value = arg.value(":output-format".len, arg_i, args) catch { + var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; + var msg_writer = err_details.msg.writer(allocator); + try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":output-format".len) }); + try diagnostics.append(err_details); + arg_i += 1; + break :next_arg; + }; + output_format = std.meta.stringToEnum(Options.OutputFormat, value.slice) orelse blk: { + var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) }; + var msg_writer = err_details.msg.writer(allocator); + try msg_writer.print("invalid output format setting: {s} ", .{value.slice}); + try diagnostics.append(err_details); + break :blk output_format; + }; + output_format_context = .{ .index = arg_i, .option_len = ":output-format".len, .arg = arg, .value = value }; + arg_i += value.index_increment; + continue :next_arg; } else if (std.ascii.startsWithIgnoreCase(arg_name, ":auto-includes")) { const value = arg.value(":auto-includes".len, arg_i, args) catch { var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; @@ -455,6 +510,25 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn }; arg_i += value.index_increment; continue :next_arg; + } else if (std.ascii.startsWithIgnoreCase(arg_name, ":input-format")) { + const value = arg.value(":input-format".len, arg_i, args) catch { + var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; + var msg_writer = err_details.msg.writer(allocator); + try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":input-format".len) }); + try diagnostics.append(err_details); + arg_i += 1; + break :next_arg; + }; + input_format = std.meta.stringToEnum(Options.InputFormat, value.slice) orelse blk: { + var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) }; + var msg_writer = err_details.msg.writer(allocator); + try msg_writer.print("invalid input format setting: {s} ", .{value.slice}); + try diagnostics.append(err_details); + break :blk input_format; + }; + input_format_context = .{ .index = arg_i, .option_len = ":input-format".len, .arg = arg, .value = value }; + arg_i += value.index_increment; + continue :next_arg; } else if (std.ascii.startsWithIgnoreCase(arg_name, ":depfile-fmt")) { const value = arg.value(":depfile-fmt".len, arg_i, args) catch { var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; @@ -489,6 +563,7 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn const path = try allocator.dupe(u8, value.slice); errdefer allocator.free(path); options.depfile_path = path; + depfile_context = .{ .index = arg_i, .option_len = ":depfile".len, .arg = arg, .value = value }; arg_i += value.index_increment; continue :next_arg; } else if (std.ascii.startsWithIgnoreCase(arg_name, "nologo")) { @@ -587,7 +662,7 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn arg_i += 1; break :next_arg; }; - output_filename_context = .{ .index = arg_i, .arg = arg, .value = value }; + output_filename_context = .{ .arg = .{ .index = arg_i, .option_len = "fo".len, .arg = arg, .value = value } }; output_filename = value.slice; arg_i += value.index_increment; continue :next_arg; @@ -779,6 +854,7 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn arg.name_offset += 1; } else if (std.ascii.startsWithIgnoreCase(arg_name, "p")) { options.preprocess = .only; + preprocess_only_context = .{ .index = arg_i, .option_len = "p".len, .arg = arg, .value = undefined }; arg.name_offset += 1; } else if (std.ascii.startsWithIgnoreCase(arg_name, "i")) { const value = arg.value(1, arg_i, args) catch { @@ -887,7 +963,7 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn if (args.len > 0) { const last_arg = args[args.len - 1]; - if (arg_i > 0 and last_arg.len > 0 and last_arg[0] == '/' and std.ascii.endsWithIgnoreCase(last_arg, ".rc")) { + if (arg_i > 0 and last_arg.len > 0 and last_arg[0] == '/' and isSupportedInputExtension(std.fs.path.extension(last_arg))) { var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = true, .arg_index = arg_i - 1 }; var note_writer = note_details.msg.writer(allocator); try note_writer.writeAll("if this argument was intended to be the input filename, then -- should be specified in front of it to exclude it from option parsing"); @@ -900,6 +976,27 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn return error.ParseError; } options.input_filename = try allocator.dupe(u8, positionals[0]); + input_filename_arg_i = arg_i; + + const InputFormatSource = enum { + inferred_from_input_filename, + input_format_arg, + }; + + var input_format_source: InputFormatSource = undefined; + if (input_format == null) { + const ext = std.fs.path.extension(options.input_filename); + if (std.ascii.eqlIgnoreCase(ext, ".res")) { + input_format = .res; + } else if (std.ascii.eqlIgnoreCase(ext, ".rcpp")) { + input_format = .rcpp; + } else { + input_format = .rc; + } + input_format_source = .inferred_from_input_filename; + } else { + input_format_source = .input_format_arg; + } if (positionals.len > 1) { if (output_filename != null) { @@ -909,47 +1006,233 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn try diagnostics.append(err_details); var note_details = Diagnostics.ErrorDetails{ .type = .note, - .arg_index = output_filename_context.value.index(output_filename_context.index), - .arg_span = output_filename_context.value.argSpan(output_filename_context.arg), + .arg_index = output_filename_context.arg.index, + .arg_span = output_filename_context.arg.value.argSpan(output_filename_context.arg.arg), }; var note_writer = note_details.msg.writer(allocator); try note_writer.writeAll("output filename previously specified here"); try diagnostics.append(note_details); } else { output_filename = positionals[1]; + output_filename_context = .{ .positional = arg_i + 1 }; } } + + const OutputFormatSource = enum { + inferred_from_input_filename, + inferred_from_output_filename, + output_format_arg, + unable_to_infer_from_input_filename, + unable_to_infer_from_output_filename, + inferred_from_preprocess_only, + }; + + var output_format_source: OutputFormatSource = undefined; if (output_filename == null) { - var buf = std.ArrayList(u8).init(allocator); - errdefer buf.deinit(); - - if (std.fs.path.dirname(options.input_filename)) |dirname| { - var end_pos = dirname.len; - // We want to ensure that we write a path separator at the end, so if the dirname - // doesn't end with a path sep then include the char after the dirname - // which must be a path sep. - if (!std.fs.path.isSep(dirname[dirname.len - 1])) end_pos += 1; - try buf.appendSlice(options.input_filename[0..end_pos]); + if (output_format == null) { + output_format_source = .inferred_from_input_filename; + const input_ext = std.fs.path.extension(options.input_filename); + if (std.ascii.eqlIgnoreCase(input_ext, ".res")) { + output_format = .coff; + } else if (options.preprocess == .only and (input_format.? == .rc or std.ascii.eqlIgnoreCase(input_ext, ".rc"))) { + output_format = .rcpp; + output_format_source = .inferred_from_preprocess_only; + } else { + if (!std.ascii.eqlIgnoreCase(input_ext, ".res")) { + output_format_source = .unable_to_infer_from_input_filename; + } + output_format = .res; + } } - try buf.appendSlice(std.fs.path.stem(options.input_filename)); - if (options.preprocess == .only) { - try buf.appendSlice(".rcpp"); + options.output_filename = try filepathWithExtension(allocator, options.input_filename, output_format.?.extension()); + } else { + options.output_filename = try allocator.dupe(u8, output_filename.?); + if (output_format == null) { + output_format_source = .inferred_from_output_filename; + const ext = std.fs.path.extension(options.output_filename); + if (std.ascii.eqlIgnoreCase(ext, ".obj") or std.ascii.eqlIgnoreCase(ext, ".o")) { + output_format = .coff; + } else if (std.ascii.eqlIgnoreCase(ext, ".rcpp")) { + output_format = .rcpp; + } else { + if (!std.ascii.eqlIgnoreCase(ext, ".res")) { + output_format_source = .unable_to_infer_from_output_filename; + } + output_format = .res; + } } else { - try buf.appendSlice(".res"); + output_format_source = .output_format_arg; } + } - options.output_filename = try buf.toOwnedSlice(); - } else { - options.output_filename = try allocator.dupe(u8, output_filename.?); + options.input_format = input_format.?; + options.output_format = output_format.?; + + // Check for incompatible options + var print_input_format_source_note: bool = false; + var print_output_format_source_note: bool = false; + if (options.depfile_path != null and (options.input_format == .res or options.output_format == .rcpp)) { + var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = depfile_context.index, .arg_span = depfile_context.value.argSpan(depfile_context.arg) }; + var msg_writer = err_details.msg.writer(allocator); + if (options.input_format == .res) { + try msg_writer.print("the {s}{s} option was ignored because the input format is '{s}'", .{ + depfile_context.arg.prefixSlice(), + depfile_context.arg.optionWithoutPrefix(depfile_context.option_len), + @tagName(options.input_format), + }); + print_input_format_source_note = true; + } else if (options.output_format == .rcpp) { + try msg_writer.print("the {s}{s} option was ignored because the output format is '{s}'", .{ + depfile_context.arg.prefixSlice(), + depfile_context.arg.optionWithoutPrefix(depfile_context.option_len), + @tagName(options.output_format), + }); + print_output_format_source_note = true; + } + try diagnostics.append(err_details); + } + if (!isSupportedTransformation(options.input_format, options.output_format)) { + var err_details = Diagnostics.ErrorDetails{ .arg_index = input_filename_arg_i, .print_args = false }; + var msg_writer = err_details.msg.writer(allocator); + try msg_writer.print("input format '{s}' cannot be converted to output format '{s}'", .{ @tagName(options.input_format), @tagName(options.output_format) }); + try diagnostics.append(err_details); + print_input_format_source_note = true; + print_output_format_source_note = true; + } + if (options.preprocess == .only and options.output_format != .rcpp) { + var err_details = Diagnostics.ErrorDetails{ .arg_index = preprocess_only_context.index }; + var msg_writer = err_details.msg.writer(allocator); + try msg_writer.print("the {s}{s} option cannot be used with output format '{s}'", .{ + preprocess_only_context.arg.prefixSlice(), + preprocess_only_context.arg.optionWithoutPrefix(preprocess_only_context.option_len), + @tagName(options.output_format), + }); + try diagnostics.append(err_details); + print_output_format_source_note = true; + } + if (print_input_format_source_note) { + switch (input_format_source) { + .inferred_from_input_filename => { + var err_details = Diagnostics.ErrorDetails{ .type = .note, .arg_index = input_filename_arg_i }; + var msg_writer = err_details.msg.writer(allocator); + try msg_writer.writeAll("the input format was inferred from the input filename"); + try diagnostics.append(err_details); + }, + .input_format_arg => { + var err_details = Diagnostics.ErrorDetails{ + .type = .note, + .arg_index = input_format_context.index, + .arg_span = input_format_context.value.argSpan(input_format_context.arg), + }; + var msg_writer = err_details.msg.writer(allocator); + try msg_writer.writeAll("the input format was specified here"); + try diagnostics.append(err_details); + }, + } + } + if (print_output_format_source_note) { + switch (output_format_source) { + .inferred_from_input_filename, .unable_to_infer_from_input_filename => { + var err_details = Diagnostics.ErrorDetails{ .type = .note, .arg_index = input_filename_arg_i }; + var msg_writer = err_details.msg.writer(allocator); + if (output_format_source == .inferred_from_input_filename) { + try msg_writer.writeAll("the output format was inferred from the input filename"); + } else { + try msg_writer.writeAll("the output format was unable to be inferred from the input filename, so the default was used"); + } + try diagnostics.append(err_details); + }, + .inferred_from_output_filename, .unable_to_infer_from_output_filename => { + var err_details: Diagnostics.ErrorDetails = switch (output_filename_context) { + .positional => |i| .{ .type = .note, .arg_index = i }, + .arg => |ctx| .{ .type = .note, .arg_index = ctx.index, .arg_span = ctx.value.argSpan(ctx.arg) }, + .unspecified => unreachable, + }; + var msg_writer = err_details.msg.writer(allocator); + if (output_format_source == .inferred_from_output_filename) { + try msg_writer.writeAll("the output format was inferred from the output filename"); + } else { + try msg_writer.writeAll("the output format was unable to be inferred from the output filename, so the default was used"); + } + try diagnostics.append(err_details); + }, + .output_format_arg => { + var err_details = Diagnostics.ErrorDetails{ + .type = .note, + .arg_index = output_format_context.index, + .arg_span = output_format_context.value.argSpan(output_format_context.arg), + }; + var msg_writer = err_details.msg.writer(allocator); + try msg_writer.writeAll("the output format was specified here"); + try diagnostics.append(err_details); + }, + .inferred_from_preprocess_only => { + var err_details = Diagnostics.ErrorDetails{ .type = .note, .arg_index = preprocess_only_context.index }; + var msg_writer = err_details.msg.writer(allocator); + try msg_writer.print("the output format was inferred from the usage of the {s}{s} option", .{ + preprocess_only_context.arg.prefixSlice(), + preprocess_only_context.arg.optionWithoutPrefix(preprocess_only_context.option_len), + }); + try diagnostics.append(err_details); + }, + } } if (diagnostics.hasError()) { return error.ParseError; } + // Implied settings from input/output formats + if (options.output_format == .rcpp) options.preprocess = .only; + if (options.input_format == .res) options.output_format = .coff; + if (options.input_format == .rcpp) options.preprocess = .no; + return options; } +pub fn filepathWithExtension(allocator: Allocator, path: []const u8, ext: []const u8) ![]const u8 { + var buf = std.ArrayList(u8).init(allocator); + errdefer buf.deinit(); + if (std.fs.path.dirname(path)) |dirname| { + var end_pos = dirname.len; + // We want to ensure that we write a path separator at the end, so if the dirname + // doesn't end with a path sep then include the char after the dirname + // which must be a path sep. + if (!std.fs.path.isSep(dirname[dirname.len - 1])) end_pos += 1; + try buf.appendSlice(path[0..end_pos]); + } + try buf.appendSlice(std.fs.path.stem(path)); + try buf.appendSlice(ext); + return try buf.toOwnedSlice(); +} + +pub fn isSupportedInputExtension(ext: []const u8) bool { + if (std.ascii.eqlIgnoreCase(ext, ".rc")) return true; + if (std.ascii.eqlIgnoreCase(ext, ".res")) return true; + if (std.ascii.eqlIgnoreCase(ext, ".rcpp")) return true; + return false; +} + +pub fn isSupportedTransformation(input: Options.InputFormat, output: Options.OutputFormat) bool { + return switch (input) { + .rc => switch (output) { + .res => true, + .coff => true, + .rcpp => true, + }, + .res => switch (output) { + .res => false, + .coff => true, + .rcpp => false, + }, + .rcpp => switch (output) { + .res => true, + .coff => true, + .rcpp => false, + }, + }; +} + /// Returns true if the str is a valid C identifier for use in a #define/#undef macro pub fn isValidIdentifier(str: []const u8) bool { for (str, 0..) |c, i| switch (c) { @@ -1250,6 +1533,28 @@ test "parse errors: basic" { \\ ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ \\ ); + try testParseError(&.{"/some/absolute/path/parsed/as/an/option.res"}, + \\: error: the /s option is unsupported + \\ ... /some/absolute/path/parsed/as/an/option.res + \\ ~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + \\: error: missing input filename + \\ + \\: note: if this argument was intended to be the input filename, then -- should be specified in front of it to exclude it from option parsing + \\ ... /some/absolute/path/parsed/as/an/option.res + \\ ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + \\ + ); + try testParseError(&.{"/some/absolute/path/parsed/as/an/option.rcpp"}, + \\: error: the /s option is unsupported + \\ ... /some/absolute/path/parsed/as/an/option.rcpp + \\ ~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + \\: error: missing input filename + \\ + \\: note: if this argument was intended to be the input filename, then -- should be specified in front of it to exclude it from option parsing + \\ ... /some/absolute/path/parsed/as/an/option.rcpp + \\ ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + \\ + ); } test "parse errors: /ln" { @@ -1465,6 +1770,183 @@ test "parse: unsupported LCX/LCE-related options" { ); } +test "parse: output filename specified twice" { + try testParseError(&.{ "/fo", "foo.res", "foo.rc", "foo.res" }, + \\: error: output filename already specified + \\ ... foo.res + \\ ^~~~~~~ + \\: note: output filename previously specified here + \\ ... /fo foo.res ... + \\ ~~~~^~~~~~~ + \\ + ); +} + +test "parse: input and output formats" { + { + try testParseError(&.{ "/:output-format", "rcpp", "foo.res" }, + \\: error: input format 'res' cannot be converted to output format 'rcpp' + \\ + \\: note: the input format was inferred from the input filename + \\ ... foo.res + \\ ^~~~~~~ + \\: note: the output format was specified here + \\ ... /:output-format rcpp ... + \\ ~~~~~~~~~~~~~~~~^~~~ + \\ + ); + } + { + try testParseError(&.{ "foo.res", "foo.rcpp" }, + \\: error: input format 'res' cannot be converted to output format 'rcpp' + \\ + \\: note: the input format was inferred from the input filename + \\ ... foo.res ... + \\ ^~~~~~~ + \\: note: the output format was inferred from the output filename + \\ ... foo.rcpp + \\ ^~~~~~~~ + \\ + ); + } + { + try testParseError(&.{ "/:input-format", "res", "foo" }, + \\: error: input format 'res' cannot be converted to output format 'res' + \\ + \\: note: the input format was specified here + \\ ... /:input-format res ... + \\ ~~~~~~~~~~~~~~~^~~ + \\: note: the output format was unable to be inferred from the input filename, so the default was used + \\ ... foo + \\ ^~~ + \\ + ); + } + { + try testParseError(&.{ "/p", "/:input-format", "res", "foo" }, + \\: error: input format 'res' cannot be converted to output format 'res' + \\ + \\: error: the /p option cannot be used with output format 'res' + \\ ... /p ... + \\ ^~ + \\: note: the input format was specified here + \\ ... /:input-format res ... + \\ ~~~~~~~~~~~~~~~^~~ + \\: note: the output format was unable to be inferred from the input filename, so the default was used + \\ ... foo + \\ ^~~ + \\ + ); + } + { + try testParseError(&.{ "/:output-format", "coff", "/p", "foo.rc" }, + \\: error: the /p option cannot be used with output format 'coff' + \\ ... /p ... + \\ ^~ + \\: note: the output format was specified here + \\ ... /:output-format coff ... + \\ ~~~~~~~~~~~~~~~~^~~~ + \\ + ); + } + { + try testParseError(&.{ "/fo", "foo.res", "/p", "foo.rc" }, + \\: error: the /p option cannot be used with output format 'res' + \\ ... /p ... + \\ ^~ + \\: note: the output format was inferred from the output filename + \\ ... /fo foo.res ... + \\ ~~~~^~~~~~~ + \\ + ); + } + { + try testParseError(&.{ "/p", "foo.rc", "foo.o" }, + \\: error: the /p option cannot be used with output format 'coff' + \\ ... /p ... + \\ ^~ + \\: note: the output format was inferred from the output filename + \\ ... foo.o + \\ ^~~~~ + \\ + ); + } + { + var options = try testParse(&.{"foo.rc"}); + defer options.deinit(); + + try std.testing.expectEqual(.rc, options.input_format); + try std.testing.expectEqual(.res, options.output_format); + } + { + var options = try testParse(&.{"foo.rcpp"}); + defer options.deinit(); + + try std.testing.expectEqual(.no, options.preprocess); + try std.testing.expectEqual(.rcpp, options.input_format); + try std.testing.expectEqual(.res, options.output_format); + } + { + var options = try testParse(&.{ "foo.rc", "foo.rcpp" }); + defer options.deinit(); + + try std.testing.expectEqual(.only, options.preprocess); + try std.testing.expectEqual(.rc, options.input_format); + try std.testing.expectEqual(.rcpp, options.output_format); + } + { + var options = try testParse(&.{ "foo.rc", "foo.obj" }); + defer options.deinit(); + + try std.testing.expectEqual(.rc, options.input_format); + try std.testing.expectEqual(.coff, options.output_format); + } + { + var options = try testParse(&.{ "/fo", "foo.o", "foo.rc" }); + defer options.deinit(); + + try std.testing.expectEqual(.rc, options.input_format); + try std.testing.expectEqual(.coff, options.output_format); + } + { + var options = try testParse(&.{"foo.res"}); + defer options.deinit(); + + try std.testing.expectEqual(.res, options.input_format); + try std.testing.expectEqual(.coff, options.output_format); + } + { + var options = try testParseWarning(&.{ "/:depfile", "foo.json", "foo.rc", "foo.rcpp" }, + \\: warning: the /:depfile option was ignored because the output format is 'rcpp' + \\ ... /:depfile foo.json ... + \\ ~~~~~~~~~~^~~~~~~~ + \\: note: the output format was inferred from the output filename + \\ ... foo.rcpp + \\ ^~~~~~~~ + \\ + ); + defer options.deinit(); + + try std.testing.expectEqual(.rc, options.input_format); + try std.testing.expectEqual(.rcpp, options.output_format); + } + { + var options = try testParseWarning(&.{ "/:depfile", "foo.json", "foo.res", "foo.o" }, + \\: warning: the /:depfile option was ignored because the input format is 'res' + \\ ... /:depfile foo.json ... + \\ ~~~~~~~~~~^~~~~~~~ + \\: note: the input format was inferred from the input filename + \\ ... foo.res ... + \\ ^~~~~~~ + \\ + ); + defer options.deinit(); + + try std.testing.expectEqual(.res, options.input_format); + try std.testing.expectEqual(.coff, options.output_format); + } +} + test "maybeAppendRC" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); diff --git a/src/main.zig b/src/main.zig index b85eea3..b1e8c50 100644 --- a/src/main.zig +++ b/src/main.zig @@ -11,6 +11,7 @@ const preprocess = @import("preprocess.zig"); const renderErrorMessage = @import("utils.zig").renderErrorMessage; const auto_includes = @import("auto_includes.zig"); const hasDisjointCodePage = @import("disjoint_code_page.zig").hasDisjointCodePage; +const cvtres = @import("cvtres.zig"); const aro = @import("aro"); pub fn main() !void { @@ -163,142 +164,203 @@ pub fn main() !void { return; } - // Note: We still want to run this when no-preprocess is set because: - // 1. We want to print accurate line numbers after removing multiline comments - // 2. We want to be able to handle an already-preprocessed input with #line commands in it - var mapping_results = parseAndRemoveLineCommands(allocator, full_input, full_input, .{ .initial_filename = options.input_filename }) catch |err| switch (err) { - error.InvalidLineCommand => { - // TODO: Maybe output the invalid line command - try renderErrorMessage(stderr.writer(), stderr_config, .err, "invalid line command in the preprocessed source", .{}); - if (options.preprocess == .no) { - try renderErrorMessage(stderr.writer(), stderr_config, .note, "line commands must be of the format: #line \"\"", .{}); - } else { - try renderErrorMessage(stderr.writer(), stderr_config, .note, "this is likely to be a bug, please report it", .{}); - } - std.process.exit(1); - }, - error.LineNumberOverflow => { - // TODO: Better error message - try renderErrorMessage(stderr.writer(), stderr_config, .err, "line number count exceeded maximum of {}", .{std.math.maxInt(usize)}); - std.process.exit(1); - }, - error.OutOfMemory => |e| return e, - }; - defer mapping_results.mappings.deinit(allocator); + const need_intermediate_res = options.output_format == .coff and options.input_format != .res; + const res_filename = if (need_intermediate_res) + try cli.filepathWithExtension(allocator, options.input_filename, ".res") + else + options.output_filename; + defer if (need_intermediate_res) allocator.free(res_filename); + + const res_data = res_data: { + if (options.input_format != .res) { + // Note: We still want to run this when no-preprocess is set because: + // 1. We want to print accurate line numbers after removing multiline comments + // 2. We want to be able to handle an already-preprocessed input with #line commands in it + var mapping_results = parseAndRemoveLineCommands(allocator, full_input, full_input, .{ .initial_filename = options.input_filename }) catch |err| switch (err) { + error.InvalidLineCommand => { + // TODO: Maybe output the invalid line command + try renderErrorMessage(stderr.writer(), stderr_config, .err, "invalid line command in the preprocessed source", .{}); + if (options.preprocess == .no) { + try renderErrorMessage(stderr.writer(), stderr_config, .note, "line commands must be of the format: #line \"\"", .{}); + } else { + try renderErrorMessage(stderr.writer(), stderr_config, .note, "this is likely to be a bug, please report it", .{}); + } + std.process.exit(1); + }, + error.LineNumberOverflow => { + // TODO: Better error message + try renderErrorMessage(stderr.writer(), stderr_config, .err, "line number count exceeded maximum of {}", .{std.math.maxInt(usize)}); + std.process.exit(1); + }, + error.OutOfMemory => |e| return e, + }; + defer mapping_results.mappings.deinit(allocator); - const default_code_page = options.default_code_page orelse .windows1252; - const has_disjoint_code_page = hasDisjointCodePage(mapping_results.result, &mapping_results.mappings, default_code_page); + const default_code_page = options.default_code_page orelse .windows1252; + const has_disjoint_code_page = hasDisjointCodePage(mapping_results.result, &mapping_results.mappings, default_code_page); - const final_input = try removeComments(mapping_results.result, mapping_results.result, &mapping_results.mappings); + const final_input = try removeComments(mapping_results.result, mapping_results.result, &mapping_results.mappings); - var output_file = std.fs.cwd().createFile(options.output_filename, .{}) catch |err| { - try renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to create output file '{s}': {s}", .{ options.output_filename, @errorName(err) }); - std.process.exit(1); - }; - var output_file_closed = false; - defer if (!output_file_closed) output_file.close(); - - var diagnostics = Diagnostics.init(allocator); - defer diagnostics.deinit(); - - if (options.debug) { - std.debug.print("disjoint code page detected: {}\n", .{has_disjoint_code_page}); - std.debug.print("after preprocessor:\n------------------\n{s}\n------------------\n", .{final_input}); - std.debug.print("\nmappings:\n", .{}); - var it = mapping_results.mappings.sources.inorderIterator(); - while (it.next()) |node| { - const source = node.key; - const filename = mapping_results.mappings.files.get(source.filename_offset); - std.debug.print("{}: {s} : {}-{}\n", .{ source.start_line, filename, source.corresponding_start_line, source.corresponding_start_line + source.span }); - } - std.debug.print("end line #: {}\n", .{mapping_results.mappings.end_line}); - std.debug.print("\n", .{}); - - // Separately parse and dump the AST - ast: { - var parse_diagnostics = Diagnostics.init(allocator); - defer parse_diagnostics.deinit(); - var lexer = lex.Lexer.init(final_input, .{}); - var parser = parse.Parser.init(&lexer, .{}); - var tree = parser.parse(allocator, &parse_diagnostics) catch { - std.debug.print("Failed to parse\n", .{}); - break :ast; + var output_file = std.fs.cwd().createFile(res_filename, .{}) catch |err| { + try renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to create output file '{s}': {s}", .{ res_filename, @errorName(err) }); + std.process.exit(1); }; - defer tree.deinit(); + var output_file_closed = false; + defer if (!output_file_closed) output_file.close(); + + var diagnostics = Diagnostics.init(allocator); + defer diagnostics.deinit(); + + if (options.debug) { + std.debug.print("disjoint code page detected: {}\n", .{has_disjoint_code_page}); + std.debug.print("after preprocessor:\n------------------\n{s}\n------------------\n", .{final_input}); + std.debug.print("\nmappings:\n", .{}); + var it = mapping_results.mappings.sources.inorderIterator(); + while (it.next()) |node| { + const source = node.key; + const filename = mapping_results.mappings.files.get(source.filename_offset); + std.debug.print("{}: {s} : {}-{}\n", .{ source.start_line, filename, source.corresponding_start_line, source.corresponding_start_line + source.span }); + } + std.debug.print("end line #: {}\n", .{mapping_results.mappings.end_line}); + std.debug.print("\n", .{}); + + // Separately parse and dump the AST + ast: { + var parse_diagnostics = Diagnostics.init(allocator); + defer parse_diagnostics.deinit(); + var lexer = lex.Lexer.init(final_input, .{}); + var parser = parse.Parser.init(&lexer, .{}); + var tree = parser.parse(allocator, &parse_diagnostics) catch { + std.debug.print("Failed to parse\n", .{}); + break :ast; + }; + defer tree.deinit(); + + try tree.dump(stderr.writer()); + std.debug.print("\n", .{}); + } + } - try tree.dump(stderr.writer()); - std.debug.print("\n", .{}); - } - } + var output_buffered_stream = std.io.bufferedWriter(output_file.writer()); + + compile(allocator, final_input, output_buffered_stream.writer(), .{ + .cwd = std.fs.cwd(), + .diagnostics = &diagnostics, + .source_mappings = &mapping_results.mappings, + .dependencies_list = maybe_dependencies_list, + .ignore_include_env_var = options.ignore_include_env_var, + .extra_include_paths = options.extra_include_paths.items, + .system_include_paths = include_paths, + .default_language_id = options.default_language_id, + .default_code_page = default_code_page, + .disjoint_code_page = has_disjoint_code_page, + .verbose = options.verbose, + .null_terminate_string_table_strings = options.null_terminate_string_table_strings, + .max_string_literal_codepoints = options.max_string_literal_codepoints, + .silent_duplicate_control_ids = options.silent_duplicate_control_ids, + .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page, + }) catch |err| switch (err) { + error.ParseError, error.CompileError => { + diagnostics.renderToStdErr(std.fs.cwd(), final_input, stderr_config, mapping_results.mappings); + // Delete the output file on error + output_file.close(); + output_file_closed = true; + // Failing to delete is not really a big deal, so swallow any errors + std.fs.cwd().deleteFile(res_filename) catch {}; + std.process.exit(1); + }, + else => |e| return e, + }; + + try output_buffered_stream.flush(); + + if (options.debug) { + std.debug.print("dependencies list:\n", .{}); + for (dependencies_list.items) |path| { + std.debug.print(" {s}\n", .{path}); + } + std.debug.print("\n", .{}); + } - var output_buffered_stream = std.io.bufferedWriter(output_file.writer()); - - compile(allocator, final_input, output_buffered_stream.writer(), .{ - .cwd = std.fs.cwd(), - .diagnostics = &diagnostics, - .source_mappings = &mapping_results.mappings, - .dependencies_list = maybe_dependencies_list, - .ignore_include_env_var = options.ignore_include_env_var, - .extra_include_paths = options.extra_include_paths.items, - .system_include_paths = include_paths, - .default_language_id = options.default_language_id, - .default_code_page = default_code_page, - .disjoint_code_page = has_disjoint_code_page, - .verbose = options.verbose, - .null_terminate_string_table_strings = options.null_terminate_string_table_strings, - .max_string_literal_codepoints = options.max_string_literal_codepoints, - .silent_duplicate_control_ids = options.silent_duplicate_control_ids, - .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page, - }) catch |err| switch (err) { - error.ParseError, error.CompileError => { + // print any warnings/notes diagnostics.renderToStdErr(std.fs.cwd(), final_input, stderr_config, mapping_results.mappings); - // Delete the output file on error - output_file.close(); - output_file_closed = true; - // Failing to delete is not really a big deal, so swallow any errors - std.fs.cwd().deleteFile(options.output_filename) catch {}; + + // write the depfile + if (options.depfile_path) |depfile_path| { + var depfile = std.fs.cwd().createFile(depfile_path, .{}) catch |err| { + try renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to create depfile '{s}': {s}", .{ depfile_path, @errorName(err) }); + std.process.exit(1); + }; + defer depfile.close(); + + const depfile_writer = depfile.writer(); + var depfile_buffered_writer = std.io.bufferedWriter(depfile_writer); + switch (options.depfile_fmt) { + .json => { + var write_stream = std.json.writeStream(depfile_buffered_writer.writer(), .{ .whitespace = .indent_2 }); + defer write_stream.deinit(); + + try write_stream.beginArray(); + for (dependencies_list.items) |dep_path| { + try write_stream.write(dep_path); + } + try write_stream.endArray(); + }, + } + try depfile_buffered_writer.flush(); + } + } + + if (options.output_format != .coff) return; + + // TODO: Maybe compile .res into memory instead of an intermediate file when output format is coff + break :res_data std.fs.cwd().readFileAlloc(allocator, res_filename, std.math.maxInt(usize)) catch |err| { + try renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to read res file path '{s}': {s}", .{ res_filename, @errorName(err) }); std.process.exit(1); - }, - else => |e| return e, + }; }; - try output_buffered_stream.flush(); + std.debug.assert(options.output_format == .coff); - if (options.debug) { - std.debug.print("dependencies list:\n", .{}); - for (dependencies_list.items) |path| { - std.debug.print(" {s}\n", .{path}); - } - std.debug.print("\n", .{}); - } + const resources = resources: { + // No need to keep the res_data around after parsing the resources from it + defer allocator.free(res_data); - // print any warnings/notes - diagnostics.renderToStdErr(std.fs.cwd(), final_input, stderr_config, mapping_results.mappings); - - // write the depfile - if (options.depfile_path) |depfile_path| { - var depfile = std.fs.cwd().createFile(depfile_path, .{}) catch |err| { - try renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to create depfile '{s}': {s}", .{ depfile_path, @errorName(err) }); + // TODO: Maybe use a buffered file reader instead of reading file into memory -> fbs + var fbs = std.io.fixedBufferStream(res_data); + break :resources cvtres.parseRes(allocator, fbs.reader(), .{ .max_size = res_data.len }) catch |err| { + // TODO: Better errors + try renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to parse res file '{s}': {s}", .{ res_filename, @errorName(err) }); std.process.exit(1); }; - defer depfile.close(); - - const depfile_writer = depfile.writer(); - var depfile_buffered_writer = std.io.bufferedWriter(depfile_writer); - switch (options.depfile_fmt) { - .json => { - var write_stream = std.json.writeStream(depfile_buffered_writer.writer(), .{ .whitespace = .indent_2 }); - defer write_stream.deinit(); - - try write_stream.beginArray(); - for (dependencies_list.items) |dep_path| { - try write_stream.write(dep_path); - } - try write_stream.endArray(); - }, - } - try depfile_buffered_writer.flush(); - } + }; + defer cvtres.freeResources(allocator, resources); + + const coff_output_filename = options.output_filename; + var coff_output_file = std.fs.cwd().createFile(coff_output_filename, .{}) catch |err| { + try renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to create output file '{s}': {s}", .{ coff_output_filename, @errorName(err) }); + std.process.exit(1); + }; + var coff_output_file_closed = false; + defer if (!coff_output_file_closed) coff_output_file.close(); + + var coff_output_buffered_stream = std.io.bufferedWriter(coff_output_file.writer()); + + cvtres.writeCoff(allocator, coff_output_buffered_stream.writer(), resources, .{ + // TODO: Make this configurable + .target = .X64, + }) catch |err| { + // TODO: Better errors + try renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to write coff output file '{s}': {s}", .{ coff_output_filename, @errorName(err) }); + // Delete the output file on error + coff_output_file.close(); + coff_output_file_closed = true; + // Failing to delete is not really a big deal, so swallow any errors + std.fs.cwd().deleteFile(coff_output_filename) catch {}; + std.process.exit(1); + }; + + try coff_output_buffered_stream.flush(); } fn getIncludePaths(allocator: std.mem.Allocator, auto_includes_option: cli.Options.AutoIncludes) ![]const []const u8 {