| author | |
| committer | |
| log | a502301b5eb84329c8c3ecb4b68bc52048f99cdc |
| tree | ad086cb9950be276b6688aef6abf6107391afb59 |
| parent | 8683f25d24414f817e946ac5fe7c9f6eceb7bb09 |
In #22522 I said:
> RC="zig rc" will now work in combination with zig cc and CMake. Here's an example of cross-compiling a simple Windows GUI CMake project
>
> $ RC="zig rc" CC="zig cc --target=x86_64-windows-gnu" cmake .. -DCMAKE_SYSTEM_NAME=Windows -G Ninja
However, I didn't realize that the time that this only works because of the `-G Ninja` part. When not using Ninja as the build tool, CMake adds a workaround for 'very long lists of object files' where it takes all object files and runs them through `ar` to combine them into one archive:
https://github.com/Kitware/CMake/blob/4a11fd8dde745789f66d6500412d7f56607e9218/Modules/Platform/Windows-GNU.cmake#L141-L158
This is a problem for the Windows resource use-case, because `ar` doesn't know how to deal with `.res` files and so this object combining step fails with:
unknown file type: foo.rc.res
Only the linker knows what to do with .res files (since it has its own `.res` -> `.obj` ('cvtres') conversion mechanism). So, when using Ninja, this object file combining step is skipped and the .res file gets passed to the linker and everyone is happy.
Note: When CMake thinks that its using `windres` as the Windows resource compiler, it will pass `-O coff` to windres which causes it to output a COFF object file instead of a `.res` file, which means that the `ar` step can succeed because it's only working on actual object files.
---
This commit gives `zig rc` the ability to output COFF object files directly when `/:output-format coff` is provided as an argument. This effectively matches what happens when CMake uses `windres` for resource compilation, but requires the argument to be provided explicitly.
So, after this change, the following CMake cross-compilation use case will work, even when not using Ninja as the generator:
RC="zig rc /:output-format coff" CC="zig cc --target=x86_64-windows-gnu" cmake .. -DCMAKE_SYSTEM_NAME=Windows3 files changed, 2013 insertions(+), 155 deletions(-)
lib/compiler/resinator/cli.zig+577-62| ... | ... | @@ -5,6 +5,7 @@ const lang = @import("lang.zig"); |
| 5 | 5 | const res = @import("res.zig"); |
| 6 | 6 | const Allocator = std.mem.Allocator; |
| 7 | 7 | const lex = @import("lex.zig"); |
| 8 | const cvtres = @import("cvtres.zig"); | |
| 8 | 9 | |
| 9 | 10 | /// This is what /SL 100 will set the maximum string literal length to |
| 10 | 11 | pub const max_string_literal_length_100_percent = 8192; |
| ... | ... | @@ -59,6 +60,20 @@ pub const usage_string_after_command_name = |
| 59 | 60 | \\ the .rc includes or otherwise depends on. |
| 60 | 61 | \\ /:depfile-fmt <value> Output format of the depfile, if /:depfile is set. |
| 61 | 62 | \\ json (default) A top-level JSON array of paths |
| 63 | \\ /:input-format <value> If not specified, the input format is inferred. | |
| 64 | \\ rc (default if input format cannot be inferred) | |
| 65 | \\ res Compiled .rc file, implies /:output-format coff | |
| 66 | \\ rcpp Preprocessed .rc file, implies /:no-preprocess | |
| 67 | \\ /:output-format <value> If not specified, the output format is inferred. | |
| 68 | \\ res (default if output format cannot be inferred) | |
| 69 | \\ coff COFF object file (extension: .obj or .o) | |
| 70 | \\ rcpp Preprocessed .rc file, implies /p | |
| 71 | \\ /:target <arch> Set the target machine for COFF object files. | |
| 72 | \\ Can be specified either as PE/COFF machine constant | |
| 73 | \\ name (X64, ARM64, etc) or Zig/LLVM CPU name (x86_64, | |
| 74 | \\ aarch64, etc). The default is X64 (aka x86_64). | |
| 75 | \\ Also accepts a full Zig/LLVM triple, but everything | |
| 76 | \\ except the architecture is ignored. | |
| 62 | 77 | \\ |
| 63 | 78 | \\Note: For compatibility reasons, all custom options start with : |
| 64 | 79 | \\ |
| ... | ... | @@ -131,8 +146,8 @@ pub const Diagnostics = struct { |
| 131 | 146 | |
| 132 | 147 | pub const Options = struct { |
| 133 | 148 | allocator: Allocator, |
| 134 | input_filename: []const u8 = &[_]u8{}, | |
| 135 | output_filename: []const u8 = &[_]u8{}, | |
| 149 | input_source: IoSource = .{ .filename = &[_]u8{} }, | |
| 150 | output_source: IoSource = .{ .filename = &[_]u8{} }, | |
| 136 | 151 | extra_include_paths: std.ArrayListUnmanaged([]const u8) = .empty, |
| 137 | 152 | ignore_include_env_var: bool = false, |
| 138 | 153 | preprocess: Preprocess = .yes, |
| ... | ... | @@ -149,9 +164,30 @@ pub const Options = struct { |
| 149 | 164 | auto_includes: AutoIncludes = .any, |
| 150 | 165 | depfile_path: ?[]const u8 = null, |
| 151 | 166 | depfile_fmt: DepfileFormat = .json, |
| 167 | input_format: InputFormat = .rc, | |
| 168 | output_format: OutputFormat = .res, | |
| 169 | coff_options: cvtres.CoffOptions = .{}, | |
| 152 | 170 | |
| 171 | pub const IoSource = union(enum) { | |
| 172 | stdio: std.fs.File, | |
| 173 | filename: []const u8, | |
| 174 | }; | |
| 153 | 175 | pub const AutoIncludes = enum { any, msvc, gnu, none }; |
| 154 | 176 | pub const DepfileFormat = enum { json }; |
| 177 | pub const InputFormat = enum { rc, res, rcpp }; | |
| 178 | pub const OutputFormat = enum { | |
| 179 | res, | |
| 180 | coff, | |
| 181 | rcpp, | |
| 182 | ||
| 183 | pub fn extension(format: OutputFormat) []const u8 { | |
| 184 | return switch (format) { | |
| 185 | .rcpp => ".rcpp", | |
| 186 | .coff => ".obj", | |
| 187 | .res => ".res", | |
| 188 | }; | |
| 189 | } | |
| 190 | }; | |
| 155 | 191 | pub const Preprocess = enum { no, yes, only }; |
| 156 | 192 | pub const SymbolAction = enum { define, undefine }; |
| 157 | 193 | pub const SymbolValue = union(SymbolAction) { |
| ... | ... | @@ -198,9 +234,10 @@ pub const Options = struct { |
| 198 | 234 | try self.symbols.put(self.allocator, duped_key, .{ .undefine = {} }); |
| 199 | 235 | } |
| 200 | 236 | |
| 201 | /// If the current input filename both: | |
| 237 | /// If the current input filename: | |
| 202 | 238 | /// - does not have an extension, and |
| 203 | /// - does not exist in the cwd | |
| 239 | /// - does not exist in the cwd, and | |
| 240 | /// - the input format is .rc | |
| 204 | 241 | /// then this function will append `.rc` to the input filename |
| 205 | 242 | /// |
| 206 | 243 | /// Note: This behavior is different from the Win32 compiler. |
| ... | ... | @@ -213,14 +250,18 @@ pub const Options = struct { |
| 213 | 250 | /// of the .rc extension being omitted from the CLI args, but still |
| 214 | 251 | /// work fine if the file itself does not have an extension. |
| 215 | 252 | pub fn maybeAppendRC(options: *Options, cwd: std.fs.Dir) !void { |
| 216 | if (std.fs.path.extension(options.input_filename).len == 0) { | |
| 217 | cwd.access(options.input_filename, .{}) catch |err| switch (err) { | |
| 253 | switch (options.input_source) { | |
| 254 | .stdio => return, | |
| 255 | .filename => {}, | |
| 256 | } | |
| 257 | if (options.input_format == .rc and std.fs.path.extension(options.input_source.filename).len == 0) { | |
| 258 | cwd.access(options.input_source.filename, .{}) catch |err| switch (err) { | |
| 218 | 259 | error.FileNotFound => { |
| 219 | var filename_bytes = try options.allocator.alloc(u8, options.input_filename.len + 3); | |
| 220 | @memcpy(filename_bytes[0..options.input_filename.len], options.input_filename); | |
| 260 | var filename_bytes = try options.allocator.alloc(u8, options.input_source.filename.len + 3); | |
| 261 | @memcpy(filename_bytes[0..options.input_source.filename.len], options.input_source.filename); | |
| 221 | 262 | @memcpy(filename_bytes[filename_bytes.len - 3 ..], ".rc"); |
| 222 | options.allocator.free(options.input_filename); | |
| 223 | options.input_filename = filename_bytes; | |
| 263 | options.allocator.free(options.input_source.filename); | |
| 264 | options.input_source = .{ .filename = filename_bytes }; | |
| 224 | 265 | }, |
| 225 | 266 | else => {}, |
| 226 | 267 | }; |
| ... | ... | @@ -232,8 +273,14 @@ pub const Options = struct { |
| 232 | 273 | self.allocator.free(extra_include_path); |
| 233 | 274 | } |
| 234 | 275 | self.extra_include_paths.deinit(self.allocator); |
| 235 | self.allocator.free(self.input_filename); | |
| 236 | self.allocator.free(self.output_filename); | |
| 276 | switch (self.input_source) { | |
| 277 | .stdio => {}, | |
| 278 | .filename => |filename| self.allocator.free(filename), | |
| 279 | } | |
| 280 | switch (self.output_source) { | |
| 281 | .stdio => {}, | |
| 282 | .filename => |filename| self.allocator.free(filename), | |
| 283 | } | |
| 237 | 284 | var symbol_it = self.symbols.iterator(); |
| 238 | 285 | while (symbol_it.next()) |entry| { |
| 239 | 286 | self.allocator.free(entry.key_ptr.*); |
| ... | ... | @@ -243,11 +290,26 @@ pub const Options = struct { |
| 243 | 290 | if (self.depfile_path) |depfile_path| { |
| 244 | 291 | self.allocator.free(depfile_path); |
| 245 | 292 | } |
| 293 | if (self.coff_options.define_external_symbol) |symbol_name| { | |
| 294 | self.allocator.free(symbol_name); | |
| 295 | } | |
| 246 | 296 | } |
| 247 | 297 | |
| 248 | 298 | pub fn dumpVerbose(self: *const Options, writer: anytype) !void { |
| 249 | try writer.print("Input filename: {s}\n", .{self.input_filename}); | |
| 250 | try writer.print("Output filename: {s}\n", .{self.output_filename}); | |
| 299 | const input_source_name = switch (self.input_source) { | |
| 300 | .stdio => "<stdin>", | |
| 301 | .filename => |filename| filename, | |
| 302 | }; | |
| 303 | const output_source_name = switch (self.output_source) { | |
| 304 | .stdio => "<stdout>", | |
| 305 | .filename => |filename| filename, | |
| 306 | }; | |
| 307 | try writer.print("Input filename: {s} (format={s})\n", .{ input_source_name, @tagName(self.input_format) }); | |
| 308 | try writer.print("Output filename: {s} (format={s})\n", .{ output_source_name, @tagName(self.output_format) }); | |
| 309 | if (self.output_format == .coff) { | |
| 310 | try writer.print(" Target machine type for COFF: {s}\n", .{@tagName(self.coff_options.target)}); | |
| 311 | } | |
| 312 | ||
| 251 | 313 | if (self.extra_include_paths.items.len > 0) { |
| 252 | 314 | try writer.writeAll(" Extra include paths:\n"); |
| 253 | 315 | for (self.extra_include_paths.items) |extra_include_path| { |
| ... | ... | @@ -331,6 +393,7 @@ pub const Arg = struct { |
| 331 | 393 | } |
| 332 | 394 | |
| 333 | 395 | pub fn optionWithoutPrefix(self: Arg, option_len: usize) []const u8 { |
| 396 | if (option_len == 0) return self.name(); | |
| 334 | 397 | return self.name()[0..option_len]; |
| 335 | 398 | } |
| 336 | 399 | |
| ... | ... | @@ -380,6 +443,8 @@ pub const Arg = struct { |
| 380 | 443 | |
| 381 | 444 | pub const Value = struct { |
| 382 | 445 | slice: []const u8, |
| 446 | /// Amount to increment the arg index to skip over both the option and the value arg(s) | |
| 447 | /// e.g. 1 if /<option><value>, 2 if /<option> <value> | |
| 383 | 448 | index_increment: u2 = 1, |
| 384 | 449 | |
| 385 | 450 | pub fn argSpan(self: Value, arg: Arg) Diagnostics.ErrorDetails.ArgSpan { |
| ... | ... | @@ -414,6 +479,7 @@ pub const Arg = struct { |
| 414 | 479 | |
| 415 | 480 | pub const Context = struct { |
| 416 | 481 | index: usize, |
| 482 | option_len: usize, | |
| 417 | 483 | arg: Arg, |
| 418 | 484 | value: Value, |
| 419 | 485 | }; |
| ... | ... | @@ -428,7 +494,18 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn |
| 428 | 494 | errdefer options.deinit(); |
| 429 | 495 | |
| 430 | 496 | var output_filename: ?[]const u8 = null; |
| 431 | var output_filename_context: Arg.Context = undefined; | |
| 497 | var output_filename_context: union(enum) { | |
| 498 | unspecified: void, | |
| 499 | positional: usize, | |
| 500 | arg: Arg.Context, | |
| 501 | } = .{ .unspecified = {} }; | |
| 502 | var output_format: ?Options.OutputFormat = null; | |
| 503 | var output_format_context: Arg.Context = undefined; | |
| 504 | var input_format: ?Options.InputFormat = null; | |
| 505 | var input_format_context: Arg.Context = undefined; | |
| 506 | var input_filename_arg_i: usize = undefined; | |
| 507 | var preprocess_only_context: Arg.Context = undefined; | |
| 508 | var depfile_context: Arg.Context = undefined; | |
| 432 | 509 | |
| 433 | 510 | var arg_i: usize = 0; |
| 434 | 511 | next_arg: while (arg_i < args.len) { |
| ... | ... | @@ -470,6 +547,25 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn |
| 470 | 547 | if (std.ascii.startsWithIgnoreCase(arg_name, ":no-preprocess")) { |
| 471 | 548 | options.preprocess = .no; |
| 472 | 549 | arg.name_offset += ":no-preprocess".len; |
| 550 | } else if (std.ascii.startsWithIgnoreCase(arg_name, ":output-format")) { | |
| 551 | const value = arg.value(":output-format".len, arg_i, args) catch { | |
| 552 | var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; | |
| 553 | var msg_writer = err_details.msg.writer(allocator); | |
| 554 | try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":output-format".len) }); | |
| 555 | try diagnostics.append(err_details); | |
| 556 | arg_i += 1; | |
| 557 | break :next_arg; | |
| 558 | }; | |
| 559 | output_format = std.meta.stringToEnum(Options.OutputFormat, value.slice) orelse blk: { | |
| 560 | var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) }; | |
| 561 | var msg_writer = err_details.msg.writer(allocator); | |
| 562 | try msg_writer.print("invalid output format setting: {s} ", .{value.slice}); | |
| 563 | try diagnostics.append(err_details); | |
| 564 | break :blk output_format; | |
| 565 | }; | |
| 566 | output_format_context = .{ .index = arg_i, .option_len = ":output-format".len, .arg = arg, .value = value }; | |
| 567 | arg_i += value.index_increment; | |
| 568 | continue :next_arg; | |
| 473 | 569 | } else if (std.ascii.startsWithIgnoreCase(arg_name, ":auto-includes")) { |
| 474 | 570 | const value = arg.value(":auto-includes".len, arg_i, args) catch { |
| 475 | 571 | var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; |
| ... | ... | @@ -488,6 +584,25 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn |
| 488 | 584 | }; |
| 489 | 585 | arg_i += value.index_increment; |
| 490 | 586 | continue :next_arg; |
| 587 | } else if (std.ascii.startsWithIgnoreCase(arg_name, ":input-format")) { | |
| 588 | const value = arg.value(":input-format".len, arg_i, args) catch { | |
| 589 | var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; | |
| 590 | var msg_writer = err_details.msg.writer(allocator); | |
| 591 | try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":input-format".len) }); | |
| 592 | try diagnostics.append(err_details); | |
| 593 | arg_i += 1; | |
| 594 | break :next_arg; | |
| 595 | }; | |
| 596 | input_format = std.meta.stringToEnum(Options.InputFormat, value.slice) orelse blk: { | |
| 597 | var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) }; | |
| 598 | var msg_writer = err_details.msg.writer(allocator); | |
| 599 | try msg_writer.print("invalid input format setting: {s} ", .{value.slice}); | |
| 600 | try diagnostics.append(err_details); | |
| 601 | break :blk input_format; | |
| 602 | }; | |
| 603 | input_format_context = .{ .index = arg_i, .option_len = ":input-format".len, .arg = arg, .value = value }; | |
| 604 | arg_i += value.index_increment; | |
| 605 | continue :next_arg; | |
| 491 | 606 | } else if (std.ascii.startsWithIgnoreCase(arg_name, ":depfile-fmt")) { |
| 492 | 607 | const value = arg.value(":depfile-fmt".len, arg_i, args) catch { |
| 493 | 608 | var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; |
| ... | ... | @@ -522,6 +637,31 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn |
| 522 | 637 | const path = try allocator.dupe(u8, value.slice); |
| 523 | 638 | errdefer allocator.free(path); |
| 524 | 639 | options.depfile_path = path; |
| 640 | depfile_context = .{ .index = arg_i, .option_len = ":depfile".len, .arg = arg, .value = value }; | |
| 641 | arg_i += value.index_increment; | |
| 642 | continue :next_arg; | |
| 643 | } else if (std.ascii.startsWithIgnoreCase(arg_name, ":target")) { | |
| 644 | const value = arg.value(":target".len, arg_i, args) catch { | |
| 645 | var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; | |
| 646 | var msg_writer = err_details.msg.writer(allocator); | |
| 647 | try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":target".len) }); | |
| 648 | try diagnostics.append(err_details); | |
| 649 | arg_i += 1; | |
| 650 | break :next_arg; | |
| 651 | }; | |
| 652 | // Take the substring up to the first dash so that a full target triple | |
| 653 | // can be used, e.g. x86_64-windows-gnu becomes x86_64 | |
| 654 | var target_it = std.mem.splitScalar(u8, value.slice, '-'); | |
| 655 | const arch_str = target_it.first(); | |
| 656 | const arch = cvtres.supported_targets.Arch.fromStringIgnoreCase(arch_str) orelse { | |
| 657 | var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) }; | |
| 658 | var msg_writer = err_details.msg.writer(allocator); | |
| 659 | try msg_writer.print("invalid or unsupported target architecture: {s}", .{arch_str}); | |
| 660 | try diagnostics.append(err_details); | |
| 661 | arg_i += value.index_increment; | |
| 662 | continue :next_arg; | |
| 663 | }; | |
| 664 | options.coff_options.target = arch.toCoffMachineType(); | |
| 525 | 665 | arg_i += value.index_increment; |
| 526 | 666 | continue :next_arg; |
| 527 | 667 | } else if (std.ascii.startsWithIgnoreCase(arg_name, "nologo")) { |
| ... | ... | @@ -620,7 +760,7 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn |
| 620 | 760 | arg_i += 1; |
| 621 | 761 | break :next_arg; |
| 622 | 762 | }; |
| 623 | output_filename_context = .{ .index = arg_i, .arg = arg, .value = value }; | |
| 763 | output_filename_context = .{ .arg = .{ .index = arg_i, .option_len = "fo".len, .arg = arg, .value = value } }; | |
| 624 | 764 | output_filename = value.slice; |
| 625 | 765 | arg_i += value.index_increment; |
| 626 | 766 | continue :next_arg; |
| ... | ... | @@ -812,6 +952,7 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn |
| 812 | 952 | arg.name_offset += 1; |
| 813 | 953 | } else if (std.ascii.startsWithIgnoreCase(arg_name, "p")) { |
| 814 | 954 | options.preprocess = .only; |
| 955 | preprocess_only_context = .{ .index = arg_i, .option_len = "p".len, .arg = arg, .value = undefined }; | |
| 815 | 956 | arg.name_offset += 1; |
| 816 | 957 | } else if (std.ascii.startsWithIgnoreCase(arg_name, "i")) { |
| 817 | 958 | const value = arg.value(1, arg_i, args) catch { |
| ... | ... | @@ -920,10 +1061,10 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn |
| 920 | 1061 | |
| 921 | 1062 | if (args.len > 0) { |
| 922 | 1063 | const last_arg = args[args.len - 1]; |
| 923 | if (arg_i > 0 and last_arg.len > 0 and last_arg[0] == '/' and std.ascii.endsWithIgnoreCase(last_arg, ".rc")) { | |
| 1064 | if (arg_i > 0 and last_arg.len > 0 and last_arg[0] == '/' and isSupportedInputExtension(std.fs.path.extension(last_arg))) { | |
| 924 | 1065 | var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = true, .arg_index = arg_i - 1 }; |
| 925 | 1066 | var note_writer = note_details.msg.writer(allocator); |
| 926 | 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"); | |
| 1067 | try note_writer.writeAll("if this argument was intended to be the input filename, adding -- in front of it will exclude it from option parsing"); | |
| 927 | 1068 | try diagnostics.append(note_details); |
| 928 | 1069 | } |
| 929 | 1070 | } |
| ... | ... | @@ -932,7 +1073,28 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn |
| 932 | 1073 | // things after this rely on the value of the input filename. |
| 933 | 1074 | return error.ParseError; |
| 934 | 1075 | } |
| 935 | options.input_filename = try allocator.dupe(u8, positionals[0]); | |
| 1076 | options.input_source = .{ .filename = try allocator.dupe(u8, positionals[0]) }; | |
| 1077 | input_filename_arg_i = arg_i; | |
| 1078 | ||
| 1079 | const InputFormatSource = enum { | |
| 1080 | inferred_from_input_filename, | |
| 1081 | input_format_arg, | |
| 1082 | }; | |
| 1083 | ||
| 1084 | var input_format_source: InputFormatSource = undefined; | |
| 1085 | if (input_format == null) { | |
| 1086 | const ext = std.fs.path.extension(options.input_source.filename); | |
| 1087 | if (std.ascii.eqlIgnoreCase(ext, ".res")) { | |
| 1088 | input_format = .res; | |
| 1089 | } else if (std.ascii.eqlIgnoreCase(ext, ".rcpp")) { | |
| 1090 | input_format = .rcpp; | |
| 1091 | } else { | |
| 1092 | input_format = .rc; | |
| 1093 | } | |
| 1094 | input_format_source = .inferred_from_input_filename; | |
| 1095 | } else { | |
| 1096 | input_format_source = .input_format_arg; | |
| 1097 | } | |
| 936 | 1098 | |
| 937 | 1099 | if (positionals.len > 1) { |
| 938 | 1100 | if (output_filename != null) { |
| ... | ... | @@ -942,53 +1104,233 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn |
| 942 | 1104 | try diagnostics.append(err_details); |
| 943 | 1105 | var note_details = Diagnostics.ErrorDetails{ |
| 944 | 1106 | .type = .note, |
| 945 | .arg_index = output_filename_context.value.index(output_filename_context.index), | |
| 946 | .arg_span = output_filename_context.value.argSpan(output_filename_context.arg), | |
| 1107 | .arg_index = output_filename_context.arg.index, | |
| 1108 | .arg_span = output_filename_context.arg.value.argSpan(output_filename_context.arg.arg), | |
| 947 | 1109 | }; |
| 948 | 1110 | var note_writer = note_details.msg.writer(allocator); |
| 949 | 1111 | try note_writer.writeAll("output filename previously specified here"); |
| 950 | 1112 | try diagnostics.append(note_details); |
| 951 | 1113 | } else { |
| 952 | 1114 | output_filename = positionals[1]; |
| 1115 | output_filename_context = .{ .positional = arg_i + 1 }; | |
| 953 | 1116 | } |
| 954 | 1117 | } |
| 1118 | ||
| 1119 | const OutputFormatSource = enum { | |
| 1120 | inferred_from_input_filename, | |
| 1121 | inferred_from_output_filename, | |
| 1122 | output_format_arg, | |
| 1123 | unable_to_infer_from_input_filename, | |
| 1124 | unable_to_infer_from_output_filename, | |
| 1125 | inferred_from_preprocess_only, | |
| 1126 | }; | |
| 1127 | ||
| 1128 | var output_format_source: OutputFormatSource = undefined; | |
| 955 | 1129 | if (output_filename == null) { |
| 956 | var buf = std.ArrayList(u8).init(allocator); | |
| 957 | errdefer buf.deinit(); | |
| 958 | ||
| 959 | if (std.fs.path.dirname(options.input_filename)) |dirname| { | |
| 960 | var end_pos = dirname.len; | |
| 961 | // We want to ensure that we write a path separator at the end, so if the dirname | |
| 962 | // doesn't end with a path sep then include the char after the dirname | |
| 963 | // which must be a path sep. | |
| 964 | if (!std.fs.path.isSep(dirname[dirname.len - 1])) end_pos += 1; | |
| 965 | try buf.appendSlice(options.input_filename[0..end_pos]); | |
| 1130 | if (output_format == null) { | |
| 1131 | output_format_source = .inferred_from_input_filename; | |
| 1132 | const input_ext = std.fs.path.extension(options.input_source.filename); | |
| 1133 | if (std.ascii.eqlIgnoreCase(input_ext, ".res")) { | |
| 1134 | output_format = .coff; | |
| 1135 | } else if (options.preprocess == .only and (input_format.? == .rc or std.ascii.eqlIgnoreCase(input_ext, ".rc"))) { | |
| 1136 | output_format = .rcpp; | |
| 1137 | output_format_source = .inferred_from_preprocess_only; | |
| 1138 | } else { | |
| 1139 | if (!std.ascii.eqlIgnoreCase(input_ext, ".res")) { | |
| 1140 | output_format_source = .unable_to_infer_from_input_filename; | |
| 1141 | } | |
| 1142 | output_format = .res; | |
| 1143 | } | |
| 966 | 1144 | } |
| 967 | try buf.appendSlice(std.fs.path.stem(options.input_filename)); | |
| 968 | if (options.preprocess == .only) { | |
| 969 | try buf.appendSlice(".rcpp"); | |
| 1145 | options.output_source = .{ .filename = try filepathWithExtension(allocator, options.input_source.filename, output_format.?.extension()) }; | |
| 1146 | } else { | |
| 1147 | options.output_source = .{ .filename = try allocator.dupe(u8, output_filename.?) }; | |
| 1148 | if (output_format == null) { | |
| 1149 | output_format_source = .inferred_from_output_filename; | |
| 1150 | const ext = std.fs.path.extension(options.output_source.filename); | |
| 1151 | if (std.ascii.eqlIgnoreCase(ext, ".obj") or std.ascii.eqlIgnoreCase(ext, ".o")) { | |
| 1152 | output_format = .coff; | |
| 1153 | } else if (std.ascii.eqlIgnoreCase(ext, ".rcpp")) { | |
| 1154 | output_format = .rcpp; | |
| 1155 | } else { | |
| 1156 | if (!std.ascii.eqlIgnoreCase(ext, ".res")) { | |
| 1157 | output_format_source = .unable_to_infer_from_output_filename; | |
| 1158 | } | |
| 1159 | output_format = .res; | |
| 1160 | } | |
| 970 | 1161 | } else { |
| 971 | try buf.appendSlice(".res"); | |
| 1162 | output_format_source = .output_format_arg; | |
| 972 | 1163 | } |
| 1164 | } | |
| 973 | 1165 | |
| 974 | options.output_filename = try buf.toOwnedSlice(); | |
| 975 | } else { | |
| 976 | options.output_filename = try allocator.dupe(u8, output_filename.?); | |
| 1166 | options.input_format = input_format.?; | |
| 1167 | options.output_format = output_format.?; | |
| 1168 | ||
| 1169 | // Check for incompatible options | |
| 1170 | var print_input_format_source_note: bool = false; | |
| 1171 | var print_output_format_source_note: bool = false; | |
| 1172 | if (options.depfile_path != null and (options.input_format == .res or options.output_format == .rcpp)) { | |
| 1173 | var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = depfile_context.index, .arg_span = depfile_context.value.argSpan(depfile_context.arg) }; | |
| 1174 | var msg_writer = err_details.msg.writer(allocator); | |
| 1175 | if (options.input_format == .res) { | |
| 1176 | try msg_writer.print("the {s}{s} option was ignored because the input format is '{s}'", .{ | |
| 1177 | depfile_context.arg.prefixSlice(), | |
| 1178 | depfile_context.arg.optionWithoutPrefix(depfile_context.option_len), | |
| 1179 | @tagName(options.input_format), | |
| 1180 | }); | |
| 1181 | print_input_format_source_note = true; | |
| 1182 | } else if (options.output_format == .rcpp) { | |
| 1183 | try msg_writer.print("the {s}{s} option was ignored because the output format is '{s}'", .{ | |
| 1184 | depfile_context.arg.prefixSlice(), | |
| 1185 | depfile_context.arg.optionWithoutPrefix(depfile_context.option_len), | |
| 1186 | @tagName(options.output_format), | |
| 1187 | }); | |
| 1188 | print_output_format_source_note = true; | |
| 1189 | } | |
| 1190 | try diagnostics.append(err_details); | |
| 1191 | } | |
| 1192 | if (!isSupportedTransformation(options.input_format, options.output_format)) { | |
| 1193 | var err_details = Diagnostics.ErrorDetails{ .arg_index = input_filename_arg_i, .print_args = false }; | |
| 1194 | var msg_writer = err_details.msg.writer(allocator); | |
| 1195 | try msg_writer.print("input format '{s}' cannot be converted to output format '{s}'", .{ @tagName(options.input_format), @tagName(options.output_format) }); | |
| 1196 | try diagnostics.append(err_details); | |
| 1197 | print_input_format_source_note = true; | |
| 1198 | print_output_format_source_note = true; | |
| 1199 | } | |
| 1200 | if (options.preprocess == .only and options.output_format != .rcpp) { | |
| 1201 | var err_details = Diagnostics.ErrorDetails{ .arg_index = preprocess_only_context.index }; | |
| 1202 | var msg_writer = err_details.msg.writer(allocator); | |
| 1203 | try msg_writer.print("the {s}{s} option cannot be used with output format '{s}'", .{ | |
| 1204 | preprocess_only_context.arg.prefixSlice(), | |
| 1205 | preprocess_only_context.arg.optionWithoutPrefix(preprocess_only_context.option_len), | |
| 1206 | @tagName(options.output_format), | |
| 1207 | }); | |
| 1208 | try diagnostics.append(err_details); | |
| 1209 | print_output_format_source_note = true; | |
| 1210 | } | |
| 1211 | if (print_input_format_source_note) { | |
| 1212 | switch (input_format_source) { | |
| 1213 | .inferred_from_input_filename => { | |
| 1214 | var err_details = Diagnostics.ErrorDetails{ .type = .note, .arg_index = input_filename_arg_i }; | |
| 1215 | var msg_writer = err_details.msg.writer(allocator); | |
| 1216 | try msg_writer.writeAll("the input format was inferred from the input filename"); | |
| 1217 | try diagnostics.append(err_details); | |
| 1218 | }, | |
| 1219 | .input_format_arg => { | |
| 1220 | var err_details = Diagnostics.ErrorDetails{ | |
| 1221 | .type = .note, | |
| 1222 | .arg_index = input_format_context.index, | |
| 1223 | .arg_span = input_format_context.value.argSpan(input_format_context.arg), | |
| 1224 | }; | |
| 1225 | var msg_writer = err_details.msg.writer(allocator); | |
| 1226 | try msg_writer.writeAll("the input format was specified here"); | |
| 1227 | try diagnostics.append(err_details); | |
| 1228 | }, | |
| 1229 | } | |
| 1230 | } | |
| 1231 | if (print_output_format_source_note) { | |
| 1232 | switch (output_format_source) { | |
| 1233 | .inferred_from_input_filename, .unable_to_infer_from_input_filename => { | |
| 1234 | var err_details = Diagnostics.ErrorDetails{ .type = .note, .arg_index = input_filename_arg_i }; | |
| 1235 | var msg_writer = err_details.msg.writer(allocator); | |
| 1236 | if (output_format_source == .inferred_from_input_filename) { | |
| 1237 | try msg_writer.writeAll("the output format was inferred from the input filename"); | |
| 1238 | } else { | |
| 1239 | try msg_writer.writeAll("the output format was unable to be inferred from the input filename, so the default was used"); | |
| 1240 | } | |
| 1241 | try diagnostics.append(err_details); | |
| 1242 | }, | |
| 1243 | .inferred_from_output_filename, .unable_to_infer_from_output_filename => { | |
| 1244 | var err_details: Diagnostics.ErrorDetails = switch (output_filename_context) { | |
| 1245 | .positional => |i| .{ .type = .note, .arg_index = i }, | |
| 1246 | .arg => |ctx| .{ .type = .note, .arg_index = ctx.index, .arg_span = ctx.value.argSpan(ctx.arg) }, | |
| 1247 | .unspecified => unreachable, | |
| 1248 | }; | |
| 1249 | var msg_writer = err_details.msg.writer(allocator); | |
| 1250 | if (output_format_source == .inferred_from_output_filename) { | |
| 1251 | try msg_writer.writeAll("the output format was inferred from the output filename"); | |
| 1252 | } else { | |
| 1253 | try msg_writer.writeAll("the output format was unable to be inferred from the output filename, so the default was used"); | |
| 1254 | } | |
| 1255 | try diagnostics.append(err_details); | |
| 1256 | }, | |
| 1257 | .output_format_arg => { | |
| 1258 | var err_details = Diagnostics.ErrorDetails{ | |
| 1259 | .type = .note, | |
| 1260 | .arg_index = output_format_context.index, | |
| 1261 | .arg_span = output_format_context.value.argSpan(output_format_context.arg), | |
| 1262 | }; | |
| 1263 | var msg_writer = err_details.msg.writer(allocator); | |
| 1264 | try msg_writer.writeAll("the output format was specified here"); | |
| 1265 | try diagnostics.append(err_details); | |
| 1266 | }, | |
| 1267 | .inferred_from_preprocess_only => { | |
| 1268 | var err_details = Diagnostics.ErrorDetails{ .type = .note, .arg_index = preprocess_only_context.index }; | |
| 1269 | var msg_writer = err_details.msg.writer(allocator); | |
| 1270 | try msg_writer.print("the output format was inferred from the usage of the {s}{s} option", .{ | |
| 1271 | preprocess_only_context.arg.prefixSlice(), | |
| 1272 | preprocess_only_context.arg.optionWithoutPrefix(preprocess_only_context.option_len), | |
| 1273 | }); | |
| 1274 | try diagnostics.append(err_details); | |
| 1275 | }, | |
| 1276 | } | |
| 977 | 1277 | } |
| 978 | 1278 | |
| 979 | 1279 | if (diagnostics.hasError()) { |
| 980 | 1280 | return error.ParseError; |
| 981 | 1281 | } |
| 982 | 1282 | |
| 1283 | // Implied settings from input/output formats | |
| 1284 | if (options.output_format == .rcpp) options.preprocess = .only; | |
| 1285 | if (options.input_format == .res) options.output_format = .coff; | |
| 1286 | if (options.input_format == .rcpp) options.preprocess = .no; | |
| 1287 | ||
| 983 | 1288 | return options; |
| 984 | 1289 | } |
| 985 | 1290 | |
| 1291 | pub fn filepathWithExtension(allocator: Allocator, path: []const u8, ext: []const u8) ![]const u8 { | |
| 1292 | var buf = std.ArrayList(u8).init(allocator); | |
| 1293 | errdefer buf.deinit(); | |
| 1294 | if (std.fs.path.dirname(path)) |dirname| { | |
| 1295 | var end_pos = dirname.len; | |
| 1296 | // We want to ensure that we write a path separator at the end, so if the dirname | |
| 1297 | // doesn't end with a path sep then include the char after the dirname | |
| 1298 | // which must be a path sep. | |
| 1299 | if (!std.fs.path.isSep(dirname[dirname.len - 1])) end_pos += 1; | |
| 1300 | try buf.appendSlice(path[0..end_pos]); | |
| 1301 | } | |
| 1302 | try buf.appendSlice(std.fs.path.stem(path)); | |
| 1303 | try buf.appendSlice(ext); | |
| 1304 | return try buf.toOwnedSlice(); | |
| 1305 | } | |
| 1306 | ||
| 986 | 1307 | pub fn isSupportedInputExtension(ext: []const u8) bool { |
| 987 | 1308 | if (std.ascii.eqlIgnoreCase(ext, ".rc")) return true; |
| 1309 | if (std.ascii.eqlIgnoreCase(ext, ".res")) return true; | |
| 988 | 1310 | if (std.ascii.eqlIgnoreCase(ext, ".rcpp")) return true; |
| 989 | 1311 | return false; |
| 990 | 1312 | } |
| 991 | 1313 | |
| 1314 | pub fn isSupportedTransformation(input: Options.InputFormat, output: Options.OutputFormat) bool { | |
| 1315 | return switch (input) { | |
| 1316 | .rc => switch (output) { | |
| 1317 | .res => true, | |
| 1318 | .coff => true, | |
| 1319 | .rcpp => true, | |
| 1320 | }, | |
| 1321 | .res => switch (output) { | |
| 1322 | .res => false, | |
| 1323 | .coff => true, | |
| 1324 | .rcpp => false, | |
| 1325 | }, | |
| 1326 | .rcpp => switch (output) { | |
| 1327 | .res => true, | |
| 1328 | .coff => true, | |
| 1329 | .rcpp => false, | |
| 1330 | }, | |
| 1331 | }; | |
| 1332 | } | |
| 1333 | ||
| 992 | 1334 | /// Returns true if the str is a valid C identifier for use in a #define/#undef macro |
| 993 | 1335 | pub fn isValidIdentifier(str: []const u8) bool { |
| 994 | 1336 | for (str, 0..) |c, i| switch (c) { |
| ... | ... | @@ -1278,17 +1620,6 @@ test "parse errors: basic" { |
| 1278 | 1620 | \\ |
| 1279 | 1621 | \\ |
| 1280 | 1622 | ); |
| 1281 | try testParseError(&.{"/some/absolute/path/parsed/as/an/option.rc"}, | |
| 1282 | \\<cli>: error: the /s option is unsupported | |
| 1283 | \\ ... /some/absolute/path/parsed/as/an/option.rc | |
| 1284 | \\ ~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| 1285 | \\<cli>: error: missing input filename | |
| 1286 | \\ | |
| 1287 | \\<cli>: 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 | |
| 1288 | \\ ... /some/absolute/path/parsed/as/an/option.rc | |
| 1289 | \\ ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| 1290 | \\ | |
| 1291 | ); | |
| 1292 | 1623 | } |
| 1293 | 1624 | |
| 1294 | 1625 | test "inferred absolute filepaths" { |
| ... | ... | @@ -1349,8 +1680,8 @@ test "parse: options" { |
| 1349 | 1680 | defer options.deinit(); |
| 1350 | 1681 | |
| 1351 | 1682 | try std.testing.expectEqual(true, options.verbose); |
| 1352 | try std.testing.expectEqualStrings("foo.rc", options.input_filename); | |
| 1353 | try std.testing.expectEqualStrings("foo.res", options.output_filename); | |
| 1683 | try std.testing.expectEqualStrings("foo.rc", options.input_source.filename); | |
| 1684 | try std.testing.expectEqualStrings("foo.res", options.output_source.filename); | |
| 1354 | 1685 | } |
| 1355 | 1686 | { |
| 1356 | 1687 | var options = try testParse(&.{ "/vx", "foo.rc" }); |
| ... | ... | @@ -1358,8 +1689,8 @@ test "parse: options" { |
| 1358 | 1689 | |
| 1359 | 1690 | try std.testing.expectEqual(true, options.verbose); |
| 1360 | 1691 | try std.testing.expectEqual(true, options.ignore_include_env_var); |
| 1361 | try std.testing.expectEqualStrings("foo.rc", options.input_filename); | |
| 1362 | try std.testing.expectEqualStrings("foo.res", options.output_filename); | |
| 1692 | try std.testing.expectEqualStrings("foo.rc", options.input_source.filename); | |
| 1693 | try std.testing.expectEqualStrings("foo.res", options.output_source.filename); | |
| 1363 | 1694 | } |
| 1364 | 1695 | { |
| 1365 | 1696 | var options = try testParse(&.{ "/xv", "foo.rc" }); |
| ... | ... | @@ -1367,8 +1698,8 @@ test "parse: options" { |
| 1367 | 1698 | |
| 1368 | 1699 | try std.testing.expectEqual(true, options.verbose); |
| 1369 | 1700 | try std.testing.expectEqual(true, options.ignore_include_env_var); |
| 1370 | try std.testing.expectEqualStrings("foo.rc", options.input_filename); | |
| 1371 | try std.testing.expectEqualStrings("foo.res", options.output_filename); | |
| 1701 | try std.testing.expectEqualStrings("foo.rc", options.input_source.filename); | |
| 1702 | try std.testing.expectEqualStrings("foo.res", options.output_source.filename); | |
| 1372 | 1703 | } |
| 1373 | 1704 | { |
| 1374 | 1705 | var options = try testParse(&.{ "/xvFObar.res", "foo.rc" }); |
| ... | ... | @@ -1376,8 +1707,8 @@ test "parse: options" { |
| 1376 | 1707 | |
| 1377 | 1708 | try std.testing.expectEqual(true, options.verbose); |
| 1378 | 1709 | try std.testing.expectEqual(true, options.ignore_include_env_var); |
| 1379 | try std.testing.expectEqualStrings("foo.rc", options.input_filename); | |
| 1380 | try std.testing.expectEqualStrings("bar.res", options.output_filename); | |
| 1710 | try std.testing.expectEqualStrings("foo.rc", options.input_source.filename); | |
| 1711 | try std.testing.expectEqualStrings("bar.res", options.output_source.filename); | |
| 1381 | 1712 | } |
| 1382 | 1713 | } |
| 1383 | 1714 | |
| ... | ... | @@ -1541,24 +1872,208 @@ test "parse: unsupported LCX/LCE-related options" { |
| 1541 | 1872 | ); |
| 1542 | 1873 | } |
| 1543 | 1874 | |
| 1875 | test "parse: output filename specified twice" { | |
| 1876 | try testParseError(&.{ "/fo", "foo.res", "foo.rc", "foo.res" }, | |
| 1877 | \\<cli>: error: output filename already specified | |
| 1878 | \\ ... foo.res | |
| 1879 | \\ ^~~~~~~ | |
| 1880 | \\<cli>: note: output filename previously specified here | |
| 1881 | \\ ... /fo foo.res ... | |
| 1882 | \\ ~~~~^~~~~~~ | |
| 1883 | \\ | |
| 1884 | ); | |
| 1885 | } | |
| 1886 | ||
| 1887 | test "parse: input and output formats" { | |
| 1888 | { | |
| 1889 | try testParseError(&.{ "/:output-format", "rcpp", "foo.res" }, | |
| 1890 | \\<cli>: error: input format 'res' cannot be converted to output format 'rcpp' | |
| 1891 | \\ | |
| 1892 | \\<cli>: note: the input format was inferred from the input filename | |
| 1893 | \\ ... foo.res | |
| 1894 | \\ ^~~~~~~ | |
| 1895 | \\<cli>: note: the output format was specified here | |
| 1896 | \\ ... /:output-format rcpp ... | |
| 1897 | \\ ~~~~~~~~~~~~~~~~^~~~ | |
| 1898 | \\ | |
| 1899 | ); | |
| 1900 | } | |
| 1901 | { | |
| 1902 | try testParseError(&.{ "foo.res", "foo.rcpp" }, | |
| 1903 | \\<cli>: error: input format 'res' cannot be converted to output format 'rcpp' | |
| 1904 | \\ | |
| 1905 | \\<cli>: note: the input format was inferred from the input filename | |
| 1906 | \\ ... foo.res ... | |
| 1907 | \\ ^~~~~~~ | |
| 1908 | \\<cli>: note: the output format was inferred from the output filename | |
| 1909 | \\ ... foo.rcpp | |
| 1910 | \\ ^~~~~~~~ | |
| 1911 | \\ | |
| 1912 | ); | |
| 1913 | } | |
| 1914 | { | |
| 1915 | try testParseError(&.{ "/:input-format", "res", "foo" }, | |
| 1916 | \\<cli>: error: input format 'res' cannot be converted to output format 'res' | |
| 1917 | \\ | |
| 1918 | \\<cli>: note: the input format was specified here | |
| 1919 | \\ ... /:input-format res ... | |
| 1920 | \\ ~~~~~~~~~~~~~~~^~~ | |
| 1921 | \\<cli>: note: the output format was unable to be inferred from the input filename, so the default was used | |
| 1922 | \\ ... foo | |
| 1923 | \\ ^~~ | |
| 1924 | \\ | |
| 1925 | ); | |
| 1926 | } | |
| 1927 | { | |
| 1928 | try testParseError(&.{ "/p", "/:input-format", "res", "foo" }, | |
| 1929 | \\<cli>: error: input format 'res' cannot be converted to output format 'res' | |
| 1930 | \\ | |
| 1931 | \\<cli>: error: the /p option cannot be used with output format 'res' | |
| 1932 | \\ ... /p ... | |
| 1933 | \\ ^~ | |
| 1934 | \\<cli>: note: the input format was specified here | |
| 1935 | \\ ... /:input-format res ... | |
| 1936 | \\ ~~~~~~~~~~~~~~~^~~ | |
| 1937 | \\<cli>: note: the output format was unable to be inferred from the input filename, so the default was used | |
| 1938 | \\ ... foo | |
| 1939 | \\ ^~~ | |
| 1940 | \\ | |
| 1941 | ); | |
| 1942 | } | |
| 1943 | { | |
| 1944 | try testParseError(&.{ "/:output-format", "coff", "/p", "foo.rc" }, | |
| 1945 | \\<cli>: error: the /p option cannot be used with output format 'coff' | |
| 1946 | \\ ... /p ... | |
| 1947 | \\ ^~ | |
| 1948 | \\<cli>: note: the output format was specified here | |
| 1949 | \\ ... /:output-format coff ... | |
| 1950 | \\ ~~~~~~~~~~~~~~~~^~~~ | |
| 1951 | \\ | |
| 1952 | ); | |
| 1953 | } | |
| 1954 | { | |
| 1955 | try testParseError(&.{ "/fo", "foo.res", "/p", "foo.rc" }, | |
| 1956 | \\<cli>: error: the /p option cannot be used with output format 'res' | |
| 1957 | \\ ... /p ... | |
| 1958 | \\ ^~ | |
| 1959 | \\<cli>: note: the output format was inferred from the output filename | |
| 1960 | \\ ... /fo foo.res ... | |
| 1961 | \\ ~~~~^~~~~~~ | |
| 1962 | \\ | |
| 1963 | ); | |
| 1964 | } | |
| 1965 | { | |
| 1966 | try testParseError(&.{ "/p", "foo.rc", "foo.o" }, | |
| 1967 | \\<cli>: error: the /p option cannot be used with output format 'coff' | |
| 1968 | \\ ... /p ... | |
| 1969 | \\ ^~ | |
| 1970 | \\<cli>: note: the output format was inferred from the output filename | |
| 1971 | \\ ... foo.o | |
| 1972 | \\ ^~~~~ | |
| 1973 | \\ | |
| 1974 | ); | |
| 1975 | } | |
| 1976 | { | |
| 1977 | var options = try testParse(&.{"foo.rc"}); | |
| 1978 | defer options.deinit(); | |
| 1979 | ||
| 1980 | try std.testing.expectEqual(.rc, options.input_format); | |
| 1981 | try std.testing.expectEqual(.res, options.output_format); | |
| 1982 | } | |
| 1983 | { | |
| 1984 | var options = try testParse(&.{"foo.rcpp"}); | |
| 1985 | defer options.deinit(); | |
| 1986 | ||
| 1987 | try std.testing.expectEqual(.no, options.preprocess); | |
| 1988 | try std.testing.expectEqual(.rcpp, options.input_format); | |
| 1989 | try std.testing.expectEqual(.res, options.output_format); | |
| 1990 | } | |
| 1991 | { | |
| 1992 | var options = try testParse(&.{ "foo.rc", "foo.rcpp" }); | |
| 1993 | defer options.deinit(); | |
| 1994 | ||
| 1995 | try std.testing.expectEqual(.only, options.preprocess); | |
| 1996 | try std.testing.expectEqual(.rc, options.input_format); | |
| 1997 | try std.testing.expectEqual(.rcpp, options.output_format); | |
| 1998 | } | |
| 1999 | { | |
| 2000 | var options = try testParse(&.{ "foo.rc", "foo.obj" }); | |
| 2001 | defer options.deinit(); | |
| 2002 | ||
| 2003 | try std.testing.expectEqual(.rc, options.input_format); | |
| 2004 | try std.testing.expectEqual(.coff, options.output_format); | |
| 2005 | } | |
| 2006 | { | |
| 2007 | var options = try testParse(&.{ "/fo", "foo.o", "foo.rc" }); | |
| 2008 | defer options.deinit(); | |
| 2009 | ||
| 2010 | try std.testing.expectEqual(.rc, options.input_format); | |
| 2011 | try std.testing.expectEqual(.coff, options.output_format); | |
| 2012 | } | |
| 2013 | { | |
| 2014 | var options = try testParse(&.{"foo.res"}); | |
| 2015 | defer options.deinit(); | |
| 2016 | ||
| 2017 | try std.testing.expectEqual(.res, options.input_format); | |
| 2018 | try std.testing.expectEqual(.coff, options.output_format); | |
| 2019 | } | |
| 2020 | { | |
| 2021 | var options = try testParseWarning(&.{ "/:depfile", "foo.json", "foo.rc", "foo.rcpp" }, | |
| 2022 | \\<cli>: warning: the /:depfile option was ignored because the output format is 'rcpp' | |
| 2023 | \\ ... /:depfile foo.json ... | |
| 2024 | \\ ~~~~~~~~~~^~~~~~~~ | |
| 2025 | \\<cli>: note: the output format was inferred from the output filename | |
| 2026 | \\ ... foo.rcpp | |
| 2027 | \\ ^~~~~~~~ | |
| 2028 | \\ | |
| 2029 | ); | |
| 2030 | defer options.deinit(); | |
| 2031 | ||
| 2032 | try std.testing.expectEqual(.rc, options.input_format); | |
| 2033 | try std.testing.expectEqual(.rcpp, options.output_format); | |
| 2034 | } | |
| 2035 | { | |
| 2036 | var options = try testParseWarning(&.{ "/:depfile", "foo.json", "foo.res", "foo.o" }, | |
| 2037 | \\<cli>: warning: the /:depfile option was ignored because the input format is 'res' | |
| 2038 | \\ ... /:depfile foo.json ... | |
| 2039 | \\ ~~~~~~~~~~^~~~~~~~ | |
| 2040 | \\<cli>: note: the input format was inferred from the input filename | |
| 2041 | \\ ... foo.res ... | |
| 2042 | \\ ^~~~~~~ | |
| 2043 | \\ | |
| 2044 | ); | |
| 2045 | defer options.deinit(); | |
| 2046 | ||
| 2047 | try std.testing.expectEqual(.res, options.input_format); | |
| 2048 | try std.testing.expectEqual(.coff, options.output_format); | |
| 2049 | } | |
| 2050 | } | |
| 2051 | ||
| 1544 | 2052 | test "maybeAppendRC" { |
| 1545 | 2053 | var tmp = std.testing.tmpDir(.{}); |
| 1546 | 2054 | defer tmp.cleanup(); |
| 1547 | 2055 | |
| 1548 | 2056 | var options = try testParse(&.{"foo"}); |
| 1549 | 2057 | defer options.deinit(); |
| 1550 | try std.testing.expectEqualStrings("foo", options.input_filename); | |
| 2058 | try std.testing.expectEqualStrings("foo", options.input_source.filename); | |
| 1551 | 2059 | |
| 1552 | 2060 | // Create the file so that it's found. In this scenario, .rc should not get |
| 1553 | 2061 | // appended. |
| 1554 | 2062 | var file = try tmp.dir.createFile("foo", .{}); |
| 1555 | 2063 | file.close(); |
| 1556 | 2064 | try options.maybeAppendRC(tmp.dir); |
| 1557 | try std.testing.expectEqualStrings("foo", options.input_filename); | |
| 2065 | try std.testing.expectEqualStrings("foo", options.input_source.filename); | |
| 1558 | 2066 | |
| 1559 | // Now delete the file and try again. Since the verbatim name is no longer found | |
| 1560 | // and the input filename does not have an extension, .rc should get appended. | |
| 2067 | // Now delete the file and try again. But this time change the input format | |
| 2068 | // to non-rc. | |
| 1561 | 2069 | try tmp.dir.deleteFile("foo"); |
| 2070 | options.input_format = .res; | |
| 2071 | try options.maybeAppendRC(tmp.dir); | |
| 2072 | try std.testing.expectEqualStrings("foo", options.input_source.filename); | |
| 2073 | ||
| 2074 | // Finally, reset the input format to rc. Since the verbatim name is no longer found | |
| 2075 | // and the input filename does not have an extension, .rc should get appended. | |
| 2076 | options.input_format = .rc; | |
| 1562 | 2077 | try options.maybeAppendRC(tmp.dir); |
| 1563 | try std.testing.expectEqualStrings("foo.rc", options.input_filename); | |
| 2078 | try std.testing.expectEqualStrings("foo.rc", options.input_source.filename); | |
| 1564 | 2079 | } |
lib/compiler/resinator/cvtres.zig created+1125| ... | ... | @@ -0,0 +1,1125 @@ |
| 1 | const std = @import("std"); | |
| 2 | const Allocator = std.mem.Allocator; | |
| 3 | const res = @import("res.zig"); | |
| 4 | const NameOrOrdinal = res.NameOrOrdinal; | |
| 5 | const MemoryFlags = res.MemoryFlags; | |
| 6 | const Language = res.Language; | |
| 7 | const numPaddingBytesNeeded = @import("compile.zig").Compiler.numPaddingBytesNeeded; | |
| 8 | ||
| 9 | pub const Resource = struct { | |
| 10 | type_value: NameOrOrdinal, | |
| 11 | name_value: NameOrOrdinal, | |
| 12 | data_version: u32, | |
| 13 | memory_flags: MemoryFlags, | |
| 14 | language: Language, | |
| 15 | version: u32, | |
| 16 | characteristics: u32, | |
| 17 | data: []const u8, | |
| 18 | ||
| 19 | pub fn deinit(self: Resource, allocator: Allocator) void { | |
| 20 | self.name_value.deinit(allocator); | |
| 21 | self.type_value.deinit(allocator); | |
| 22 | allocator.free(self.data); | |
| 23 | } | |
| 24 | ||
| 25 | /// Returns true if all fields match the expected value of the resource at the | |
| 26 | /// start of all .res files that distinguishes the .res file as 32-bit (as | |
| 27 | /// opposed to 16-bit). | |
| 28 | pub fn is32BitPreface(self: Resource) bool { | |
| 29 | if (self.type_value != .ordinal or self.type_value.ordinal != 0) return false; | |
| 30 | if (self.name_value != .ordinal or self.name_value.ordinal != 0) return false; | |
| 31 | if (self.data_version != 0) return false; | |
| 32 | if (@as(u16, @bitCast(self.memory_flags)) != 0) return false; | |
| 33 | if (@as(u16, @bitCast(self.language)) != 0) return false; | |
| 34 | if (self.version != 0) return false; | |
| 35 | if (self.characteristics != 0) return false; | |
| 36 | if (self.data.len != 0) return false; | |
| 37 | return true; | |
| 38 | } | |
| 39 | ||
| 40 | pub fn isDlgInclude(resource: Resource) bool { | |
| 41 | return resource.type_value == .ordinal and resource.type_value.ordinal == @intFromEnum(res.RT.DLGINCLUDE); | |
| 42 | } | |
| 43 | }; | |
| 44 | ||
| 45 | pub const ParsedResources = struct { | |
| 46 | list: std.ArrayListUnmanaged(Resource) = .empty, | |
| 47 | allocator: Allocator, | |
| 48 | ||
| 49 | pub fn init(allocator: Allocator) ParsedResources { | |
| 50 | return .{ .allocator = allocator }; | |
| 51 | } | |
| 52 | ||
| 53 | pub fn deinit(self: *ParsedResources) void { | |
| 54 | for (self.list.items) |*resource| { | |
| 55 | resource.deinit(self.allocator); | |
| 56 | } | |
| 57 | self.list.deinit(self.allocator); | |
| 58 | } | |
| 59 | }; | |
| 60 | ||
| 61 | pub const ParseResOptions = struct { | |
| 62 | skip_zero_data_resources: bool = true, | |
| 63 | skip_dlginclude_resources: bool = true, | |
| 64 | max_size: u64, | |
| 65 | }; | |
| 66 | ||
| 67 | /// The returned ParsedResources should be freed by calling its `deinit` function. | |
| 68 | pub fn parseRes(allocator: Allocator, reader: anytype, options: ParseResOptions) !ParsedResources { | |
| 69 | var resources = ParsedResources.init(allocator); | |
| 70 | errdefer resources.deinit(); | |
| 71 | ||
| 72 | try parseResInto(&resources, reader, options); | |
| 73 | ||
| 74 | return resources; | |
| 75 | } | |
| 76 | ||
| 77 | pub fn parseResInto(resources: *ParsedResources, reader: anytype, options: ParseResOptions) !void { | |
| 78 | const allocator = resources.allocator; | |
| 79 | var bytes_remaining: u64 = options.max_size; | |
| 80 | { | |
| 81 | const first_resource_and_size = try parseResource(allocator, reader, bytes_remaining); | |
| 82 | defer first_resource_and_size.resource.deinit(allocator); | |
| 83 | if (!first_resource_and_size.resource.is32BitPreface()) return error.InvalidPreface; | |
| 84 | bytes_remaining -= first_resource_and_size.total_size; | |
| 85 | } | |
| 86 | ||
| 87 | while (bytes_remaining != 0) { | |
| 88 | const resource_and_size = try parseResource(allocator, reader, bytes_remaining); | |
| 89 | if (options.skip_zero_data_resources and resource_and_size.resource.data.len == 0) { | |
| 90 | resource_and_size.resource.deinit(allocator); | |
| 91 | } else if (options.skip_dlginclude_resources and resource_and_size.resource.isDlgInclude()) { | |
| 92 | resource_and_size.resource.deinit(allocator); | |
| 93 | } else { | |
| 94 | errdefer resource_and_size.resource.deinit(allocator); | |
| 95 | try resources.list.append(allocator, resource_and_size.resource); | |
| 96 | } | |
| 97 | bytes_remaining -= resource_and_size.total_size; | |
| 98 | } | |
| 99 | } | |
| 100 | ||
| 101 | pub const ResourceAndSize = struct { | |
| 102 | resource: Resource, | |
| 103 | total_size: u64, | |
| 104 | }; | |
| 105 | ||
| 106 | pub fn parseResource(allocator: Allocator, reader: anytype, max_size: u64) !ResourceAndSize { | |
| 107 | var header_counting_reader = std.io.countingReader(reader); | |
| 108 | const header_reader = header_counting_reader.reader(); | |
| 109 | const data_size = try header_reader.readInt(u32, .little); | |
| 110 | const header_size = try header_reader.readInt(u32, .little); | |
| 111 | const total_size: u64 = @as(u64, header_size) + data_size; | |
| 112 | if (total_size > max_size) return error.ImpossibleSize; | |
| 113 | ||
| 114 | var header_bytes_available = header_size -| 8; | |
| 115 | var type_reader = std.io.limitedReader(header_reader, header_bytes_available); | |
| 116 | const type_value = try parseNameOrOrdinal(allocator, type_reader.reader()); | |
| 117 | errdefer type_value.deinit(allocator); | |
| 118 | ||
| 119 | header_bytes_available -|= @intCast(type_value.byteLen()); | |
| 120 | var name_reader = std.io.limitedReader(header_reader, header_bytes_available); | |
| 121 | const name_value = try parseNameOrOrdinal(allocator, name_reader.reader()); | |
| 122 | errdefer name_value.deinit(allocator); | |
| 123 | ||
| 124 | const padding_after_name = numPaddingBytesNeeded(@intCast(header_counting_reader.bytes_read)); | |
| 125 | try header_reader.skipBytes(padding_after_name, .{ .buf_size = 3 }); | |
| 126 | ||
| 127 | std.debug.assert(header_counting_reader.bytes_read % 4 == 0); | |
| 128 | const data_version = try header_reader.readInt(u32, .little); | |
| 129 | const memory_flags: MemoryFlags = @bitCast(try header_reader.readInt(u16, .little)); | |
| 130 | const language: Language = @bitCast(try header_reader.readInt(u16, .little)); | |
| 131 | const version = try header_reader.readInt(u32, .little); | |
| 132 | const characteristics = try header_reader.readInt(u32, .little); | |
| 133 | ||
| 134 | const header_bytes_read = header_counting_reader.bytes_read; | |
| 135 | if (header_size != header_bytes_read) return error.HeaderSizeMismatch; | |
| 136 | ||
| 137 | const data = try allocator.alloc(u8, data_size); | |
| 138 | errdefer allocator.free(data); | |
| 139 | try reader.readNoEof(data); | |
| 140 | ||
| 141 | const padding_after_data = numPaddingBytesNeeded(@intCast(data_size)); | |
| 142 | try reader.skipBytes(padding_after_data, .{ .buf_size = 3 }); | |
| 143 | ||
| 144 | return .{ | |
| 145 | .resource = .{ | |
| 146 | .name_value = name_value, | |
| 147 | .type_value = type_value, | |
| 148 | .language = language, | |
| 149 | .memory_flags = memory_flags, | |
| 150 | .version = version, | |
| 151 | .characteristics = characteristics, | |
| 152 | .data_version = data_version, | |
| 153 | .data = data, | |
| 154 | }, | |
| 155 | .total_size = header_size + data.len + padding_after_data, | |
| 156 | }; | |
| 157 | } | |
| 158 | ||
| 159 | pub fn parseNameOrOrdinal(allocator: Allocator, reader: anytype) !NameOrOrdinal { | |
| 160 | const first_code_unit = try reader.readInt(u16, .little); | |
| 161 | if (first_code_unit == 0xFFFF) { | |
| 162 | const ordinal_value = try reader.readInt(u16, .little); | |
| 163 | return .{ .ordinal = ordinal_value }; | |
| 164 | } | |
| 165 | var name_buf = try std.ArrayListUnmanaged(u16).initCapacity(allocator, 16); | |
| 166 | errdefer name_buf.deinit(allocator); | |
| 167 | var code_unit = first_code_unit; | |
| 168 | while (code_unit != 0) { | |
| 169 | try name_buf.append(allocator, std.mem.nativeToLittle(u16, code_unit)); | |
| 170 | code_unit = try reader.readInt(u16, .little); | |
| 171 | } | |
| 172 | return .{ .name = try name_buf.toOwnedSliceSentinel(allocator, 0) }; | |
| 173 | } | |
| 174 | ||
| 175 | pub const CoffOptions = struct { | |
| 176 | target: std.coff.MachineType = .X64, | |
| 177 | /// If true, zeroes will be written to all timestamp fields | |
| 178 | reproducible: bool = true, | |
| 179 | /// If true, the MEM_WRITE flag will not be set in the .rsrc section header | |
| 180 | read_only: bool = false, | |
| 181 | /// If non-null, a symbol with this name and storage class EXTERNAL will be added to the symbol table. | |
| 182 | define_external_symbol: ?[]const u8 = null, | |
| 183 | /// Re-use data offsets for resources with data that is identical. | |
| 184 | fold_duplicate_data: bool = false, | |
| 185 | }; | |
| 186 | ||
| 187 | pub const Diagnostics = union { | |
| 188 | none: void, | |
| 189 | /// Contains the index of the second resource in a duplicate resource pair. | |
| 190 | duplicate_resource: usize, | |
| 191 | /// Contains the index of the resource that either has data that's too long or | |
| 192 | /// caused the total data to overflow. | |
| 193 | overflow_resource: usize, | |
| 194 | }; | |
| 195 | ||
| 196 | pub fn writeCoff(allocator: Allocator, writer: anytype, resources: []const Resource, options: CoffOptions, diagnostics: ?*Diagnostics) !void { | |
| 197 | var resource_tree = ResourceTree.init(allocator, options); | |
| 198 | defer resource_tree.deinit(); | |
| 199 | ||
| 200 | for (resources, 0..) |*resource, i| { | |
| 201 | resource_tree.put(resource, i) catch |err| { | |
| 202 | switch (err) { | |
| 203 | error.DuplicateResource => { | |
| 204 | if (diagnostics) |d_ptr| d_ptr.* = .{ .duplicate_resource = i }; | |
| 205 | }, | |
| 206 | error.ResourceDataTooLong, error.TotalResourceDataTooLong => { | |
| 207 | if (diagnostics) |d_ptr| d_ptr.* = .{ .overflow_resource = i }; | |
| 208 | }, | |
| 209 | else => {}, | |
| 210 | } | |
| 211 | return err; | |
| 212 | }; | |
| 213 | } | |
| 214 | ||
| 215 | const lengths = resource_tree.dataLengths(); | |
| 216 | const byte_size_of_relocation = 10; | |
| 217 | const relocations_len: u32 = @intCast(byte_size_of_relocation * resources.len); | |
| 218 | const pointer_to_rsrc01_data = @sizeOf(std.coff.CoffHeader) + (@sizeOf(std.coff.SectionHeader) * 2); | |
| 219 | const pointer_to_relocations = pointer_to_rsrc01_data + lengths.rsrc01; | |
| 220 | const pointer_to_rsrc02_data = pointer_to_relocations + relocations_len; | |
| 221 | const pointer_to_symbol_table = pointer_to_rsrc02_data + lengths.rsrc02; | |
| 222 | ||
| 223 | const timestamp: i64 = if (options.reproducible) 0 else std.time.timestamp(); | |
| 224 | const size_of_optional_header = 0; | |
| 225 | const machine_type: std.coff.MachineType = options.target; | |
| 226 | const flags = std.coff.CoffHeaderFlags{ | |
| 227 | .@"32BIT_MACHINE" = 1, | |
| 228 | }; | |
| 229 | const number_of_symbols = 5 + @as(u32, @intCast(resources.len)) + @intFromBool(options.define_external_symbol != null); | |
| 230 | const coff_header = std.coff.CoffHeader{ | |
| 231 | .machine = machine_type, | |
| 232 | .number_of_sections = 2, | |
| 233 | .time_date_stamp = @as(u32, @truncate(@as(u64, @bitCast(timestamp)))), | |
| 234 | .pointer_to_symbol_table = pointer_to_symbol_table, | |
| 235 | .number_of_symbols = number_of_symbols, | |
| 236 | .size_of_optional_header = size_of_optional_header, | |
| 237 | .flags = flags, | |
| 238 | }; | |
| 239 | ||
| 240 | try writer.writeStructEndian(coff_header, .little); | |
| 241 | ||
| 242 | const rsrc01_header = std.coff.SectionHeader{ | |
| 243 | .name = ".rsrc$01".*, | |
| 244 | .virtual_size = 0, | |
| 245 | .virtual_address = 0, | |
| 246 | .size_of_raw_data = lengths.rsrc01, | |
| 247 | .pointer_to_raw_data = pointer_to_rsrc01_data, | |
| 248 | .pointer_to_relocations = if (relocations_len != 0) pointer_to_relocations else 0, | |
| 249 | .pointer_to_linenumbers = 0, | |
| 250 | .number_of_relocations = @intCast(resources.len), | |
| 251 | .number_of_linenumbers = 0, | |
| 252 | .flags = .{ | |
| 253 | .CNT_INITIALIZED_DATA = 1, | |
| 254 | .MEM_WRITE = @intFromBool(!options.read_only), | |
| 255 | .MEM_READ = 1, | |
| 256 | }, | |
| 257 | }; | |
| 258 | try writer.writeStructEndian(rsrc01_header, .little); | |
| 259 | ||
| 260 | const rsrc02_header = std.coff.SectionHeader{ | |
| 261 | .name = ".rsrc$02".*, | |
| 262 | .virtual_size = 0, | |
| 263 | .virtual_address = 0, | |
| 264 | .size_of_raw_data = lengths.rsrc02, | |
| 265 | .pointer_to_raw_data = pointer_to_rsrc02_data, | |
| 266 | .pointer_to_relocations = 0, | |
| 267 | .pointer_to_linenumbers = 0, | |
| 268 | .number_of_relocations = 0, | |
| 269 | .number_of_linenumbers = 0, | |
| 270 | .flags = .{ | |
| 271 | .CNT_INITIALIZED_DATA = 1, | |
| 272 | .MEM_WRITE = @intFromBool(!options.read_only), | |
| 273 | .MEM_READ = 1, | |
| 274 | }, | |
| 275 | }; | |
| 276 | try writer.writeStructEndian(rsrc02_header, .little); | |
| 277 | ||
| 278 | // TODO: test surrogate pairs | |
| 279 | try resource_tree.sort(); | |
| 280 | ||
| 281 | var string_table = StringTable{}; | |
| 282 | defer string_table.deinit(allocator); | |
| 283 | const resource_symbols = try resource_tree.writeCoff( | |
| 284 | allocator, | |
| 285 | writer, | |
| 286 | resources, | |
| 287 | lengths, | |
| 288 | &string_table, | |
| 289 | ); | |
| 290 | defer allocator.free(resource_symbols); | |
| 291 | ||
| 292 | try writeSymbol(writer, .{ | |
| 293 | .name = "@feat.00".*, | |
| 294 | .value = 0x11, | |
| 295 | .section_number = .ABSOLUTE, | |
| 296 | .type = .{ | |
| 297 | .base_type = .NULL, | |
| 298 | .complex_type = .NULL, | |
| 299 | }, | |
| 300 | .storage_class = .STATIC, | |
| 301 | .number_of_aux_symbols = 0, | |
| 302 | }); | |
| 303 | ||
| 304 | try writeSymbol(writer, .{ | |
| 305 | .name = ".rsrc$01".*, | |
| 306 | .value = 0, | |
| 307 | .section_number = @enumFromInt(1), | |
| 308 | .type = .{ | |
| 309 | .base_type = .NULL, | |
| 310 | .complex_type = .NULL, | |
| 311 | }, | |
| 312 | .storage_class = .STATIC, | |
| 313 | .number_of_aux_symbols = 1, | |
| 314 | }); | |
| 315 | try writeSectionDefinition(writer, .{ | |
| 316 | .length = lengths.rsrc01, | |
| 317 | .number_of_relocations = @intCast(resources.len), | |
| 318 | .number_of_linenumbers = 0, | |
| 319 | .checksum = 0, | |
| 320 | .number = 0, | |
| 321 | .selection = .NONE, | |
| 322 | .unused = .{0} ** 3, | |
| 323 | }); | |
| 324 | ||
| 325 | try writeSymbol(writer, .{ | |
| 326 | .name = ".rsrc$02".*, | |
| 327 | .value = 0, | |
| 328 | .section_number = @enumFromInt(2), | |
| 329 | .type = .{ | |
| 330 | .base_type = .NULL, | |
| 331 | .complex_type = .NULL, | |
| 332 | }, | |
| 333 | .storage_class = .STATIC, | |
| 334 | .number_of_aux_symbols = 1, | |
| 335 | }); | |
| 336 | try writeSectionDefinition(writer, .{ | |
| 337 | .length = lengths.rsrc02, | |
| 338 | .number_of_relocations = 0, | |
| 339 | .number_of_linenumbers = 0, | |
| 340 | .checksum = 0, | |
| 341 | .number = 0, | |
| 342 | .selection = .NONE, | |
| 343 | .unused = .{0} ** 3, | |
| 344 | }); | |
| 345 | ||
| 346 | for (resource_symbols) |resource_symbol| { | |
| 347 | try writeSymbol(writer, resource_symbol); | |
| 348 | } | |
| 349 | ||
| 350 | if (options.define_external_symbol) |external_symbol_name| { | |
| 351 | const name_bytes: [8]u8 = name_bytes: { | |
| 352 | if (external_symbol_name.len > 8) { | |
| 353 | const string_table_offset: u32 = try string_table.put(allocator, external_symbol_name); | |
| 354 | var bytes = [_]u8{0} ** 8; | |
| 355 | std.mem.writeInt(u32, bytes[4..8], string_table_offset, .little); | |
| 356 | break :name_bytes bytes; | |
| 357 | } else { | |
| 358 | var symbol_shortname = [_]u8{0} ** 8; | |
| 359 | @memcpy(symbol_shortname[0..external_symbol_name.len], external_symbol_name); | |
| 360 | break :name_bytes symbol_shortname; | |
| 361 | } | |
| 362 | }; | |
| 363 | ||
| 364 | try writeSymbol(writer, .{ | |
| 365 | .name = name_bytes, | |
| 366 | .value = 0, | |
| 367 | .section_number = .ABSOLUTE, | |
| 368 | .type = .{ | |
| 369 | .base_type = .NULL, | |
| 370 | .complex_type = .NULL, | |
| 371 | }, | |
| 372 | .storage_class = .EXTERNAL, | |
| 373 | .number_of_aux_symbols = 0, | |
| 374 | }); | |
| 375 | } | |
| 376 | ||
| 377 | try writer.writeInt(u32, string_table.totalByteLength(), .little); | |
| 378 | try writer.writeAll(string_table.bytes.items); | |
| 379 | } | |
| 380 | ||
| 381 | fn writeSymbol(writer: anytype, symbol: std.coff.Symbol) !void { | |
| 382 | try writer.writeAll(&symbol.name); | |
| 383 | try writer.writeInt(u32, symbol.value, .little); | |
| 384 | try writer.writeInt(u16, @intFromEnum(symbol.section_number), .little); | |
| 385 | try writer.writeInt(u8, @intFromEnum(symbol.type.base_type), .little); | |
| 386 | try writer.writeInt(u8, @intFromEnum(symbol.type.complex_type), .little); | |
| 387 | try writer.writeInt(u8, @intFromEnum(symbol.storage_class), .little); | |
| 388 | try writer.writeInt(u8, symbol.number_of_aux_symbols, .little); | |
| 389 | } | |
| 390 | ||
| 391 | fn writeSectionDefinition(writer: anytype, def: std.coff.SectionDefinition) !void { | |
| 392 | try writer.writeInt(u32, def.length, .little); | |
| 393 | try writer.writeInt(u16, def.number_of_relocations, .little); | |
| 394 | try writer.writeInt(u16, def.number_of_linenumbers, .little); | |
| 395 | try writer.writeInt(u32, def.checksum, .little); | |
| 396 | try writer.writeInt(u16, def.number, .little); | |
| 397 | try writer.writeInt(u8, @intFromEnum(def.selection), .little); | |
| 398 | try writer.writeAll(&def.unused); | |
| 399 | } | |
| 400 | ||
| 401 | pub const ResourceDirectoryTable = extern struct { | |
| 402 | characteristics: u32, | |
| 403 | timestamp: u32, | |
| 404 | major_version: u16, | |
| 405 | minor_version: u16, | |
| 406 | number_of_name_entries: u16, | |
| 407 | number_of_id_entries: u16, | |
| 408 | }; | |
| 409 | ||
| 410 | pub const ResourceDirectoryEntry = extern struct { | |
| 411 | entry: packed union { | |
| 412 | name_offset: packed struct(u32) { | |
| 413 | address: u31, | |
| 414 | /// This is undocumented in the PE/COFF spec, but the high bit | |
| 415 | /// is set by cvtres.exe for string addresses | |
| 416 | to_string: bool = true, | |
| 417 | }, | |
| 418 | integer_id: u32, | |
| 419 | }, | |
| 420 | offset: packed struct(u32) { | |
| 421 | address: u31, | |
| 422 | to_subdirectory: bool, | |
| 423 | }, | |
| 424 | ||
| 425 | pub fn writeCoff(self: ResourceDirectoryEntry, writer: anytype) !void { | |
| 426 | try writer.writeInt(u32, @bitCast(self.entry), .little); | |
| 427 | try writer.writeInt(u32, @bitCast(self.offset), .little); | |
| 428 | } | |
| 429 | }; | |
| 430 | ||
| 431 | pub const ResourceDataEntry = extern struct { | |
| 432 | data_rva: u32, | |
| 433 | size: u32, | |
| 434 | codepage: u32, | |
| 435 | reserved: u32 = 0, | |
| 436 | }; | |
| 437 | ||
| 438 | /// type -> name -> language | |
| 439 | const ResourceTree = struct { | |
| 440 | type_to_name_map: std.ArrayHashMapUnmanaged(NameOrOrdinal, NameToLanguageMap, NameOrOrdinalHashContext, true), | |
| 441 | rsrc_string_table: std.ArrayHashMapUnmanaged(NameOrOrdinal, void, NameOrOrdinalHashContext, true), | |
| 442 | deduplicated_data: std.StringArrayHashMapUnmanaged(u32), | |
| 443 | data_offsets: std.ArrayListUnmanaged(u32), | |
| 444 | rsrc02_len: u32, | |
| 445 | coff_options: CoffOptions, | |
| 446 | allocator: Allocator, | |
| 447 | ||
| 448 | const RelocatableResource = struct { | |
| 449 | resource: *const Resource, | |
| 450 | original_index: usize, | |
| 451 | }; | |
| 452 | const LanguageToResourceMap = std.AutoArrayHashMapUnmanaged(Language, RelocatableResource); | |
| 453 | const NameToLanguageMap = std.ArrayHashMapUnmanaged(NameOrOrdinal, LanguageToResourceMap, NameOrOrdinalHashContext, true); | |
| 454 | ||
| 455 | const NameOrOrdinalHashContext = struct { | |
| 456 | pub fn hash(self: @This(), v: NameOrOrdinal) u32 { | |
| 457 | _ = self; | |
| 458 | var hasher = std.hash.Wyhash.init(0); | |
| 459 | const tag = std.meta.activeTag(v); | |
| 460 | hasher.update(std.mem.asBytes(&tag)); | |
| 461 | switch (v) { | |
| 462 | .name => |name| { | |
| 463 | hasher.update(std.mem.sliceAsBytes(name)); | |
| 464 | }, | |
| 465 | .ordinal => |*ordinal| { | |
| 466 | hasher.update(std.mem.asBytes(ordinal)); | |
| 467 | }, | |
| 468 | } | |
| 469 | return @truncate(hasher.final()); | |
| 470 | } | |
| 471 | pub fn eql(self: @This(), a: NameOrOrdinal, b: NameOrOrdinal, b_index: usize) bool { | |
| 472 | _ = self; | |
| 473 | _ = b_index; | |
| 474 | const tag_a = std.meta.activeTag(a); | |
| 475 | const tag_b = std.meta.activeTag(b); | |
| 476 | if (tag_a != tag_b) return false; | |
| 477 | ||
| 478 | return switch (a) { | |
| 479 | .name => std.mem.eql(u16, a.name, b.name), | |
| 480 | .ordinal => a.ordinal == b.ordinal, | |
| 481 | }; | |
| 482 | } | |
| 483 | }; | |
| 484 | ||
| 485 | pub fn init(allocator: Allocator, coff_options: CoffOptions) ResourceTree { | |
| 486 | return .{ | |
| 487 | .type_to_name_map = .empty, | |
| 488 | .rsrc_string_table = .empty, | |
| 489 | .deduplicated_data = .empty, | |
| 490 | .data_offsets = .empty, | |
| 491 | .rsrc02_len = 0, | |
| 492 | .coff_options = coff_options, | |
| 493 | .allocator = allocator, | |
| 494 | }; | |
| 495 | } | |
| 496 | ||
| 497 | pub fn deinit(self: *ResourceTree) void { | |
| 498 | for (self.type_to_name_map.values()) |*name_to_lang_map| { | |
| 499 | for (name_to_lang_map.values()) |*lang_to_resources_map| { | |
| 500 | lang_to_resources_map.deinit(self.allocator); | |
| 501 | } | |
| 502 | name_to_lang_map.deinit(self.allocator); | |
| 503 | } | |
| 504 | self.type_to_name_map.deinit(self.allocator); | |
| 505 | self.rsrc_string_table.deinit(self.allocator); | |
| 506 | self.deduplicated_data.deinit(self.allocator); | |
| 507 | self.data_offsets.deinit(self.allocator); | |
| 508 | } | |
| 509 | ||
| 510 | pub fn put(self: *ResourceTree, resource: *const Resource, original_index: usize) !void { | |
| 511 | const name_to_lang_map = blk: { | |
| 512 | const gop_result = try self.type_to_name_map.getOrPut(self.allocator, resource.type_value); | |
| 513 | if (!gop_result.found_existing) { | |
| 514 | gop_result.value_ptr.* = .empty; | |
| 515 | } | |
| 516 | break :blk gop_result.value_ptr; | |
| 517 | }; | |
| 518 | const lang_to_resources_map = blk: { | |
| 519 | const gop_result = try name_to_lang_map.getOrPut(self.allocator, resource.name_value); | |
| 520 | if (!gop_result.found_existing) { | |
| 521 | gop_result.value_ptr.* = .empty; | |
| 522 | } | |
| 523 | break :blk gop_result.value_ptr; | |
| 524 | }; | |
| 525 | { | |
| 526 | const gop_result = try lang_to_resources_map.getOrPut(self.allocator, resource.language); | |
| 527 | if (gop_result.found_existing) return error.DuplicateResource; | |
| 528 | gop_result.value_ptr.* = .{ | |
| 529 | .original_index = original_index, | |
| 530 | .resource = resource, | |
| 531 | }; | |
| 532 | } | |
| 533 | ||
| 534 | // Resize the data_offsets list to accommodate the index, but only if necessary | |
| 535 | try self.data_offsets.resize(self.allocator, @max(self.data_offsets.items.len, original_index + 1)); | |
| 536 | if (self.coff_options.fold_duplicate_data) { | |
| 537 | const gop_result = try self.deduplicated_data.getOrPut(self.allocator, resource.data); | |
| 538 | if (!gop_result.found_existing) { | |
| 539 | gop_result.value_ptr.* = self.rsrc02_len; | |
| 540 | try self.incrementRsrc02Len(resource); | |
| 541 | } | |
| 542 | self.data_offsets.items[original_index] = gop_result.value_ptr.*; | |
| 543 | } else { | |
| 544 | self.data_offsets.items[original_index] = self.rsrc02_len; | |
| 545 | try self.incrementRsrc02Len(resource); | |
| 546 | } | |
| 547 | ||
| 548 | if (resource.type_value == .name and !self.rsrc_string_table.contains(resource.type_value)) { | |
| 549 | try self.rsrc_string_table.putNoClobber(self.allocator, resource.type_value, {}); | |
| 550 | } | |
| 551 | if (resource.name_value == .name and !self.rsrc_string_table.contains(resource.name_value)) { | |
| 552 | try self.rsrc_string_table.putNoClobber(self.allocator, resource.name_value, {}); | |
| 553 | } | |
| 554 | } | |
| 555 | ||
| 556 | fn incrementRsrc02Len(self: *ResourceTree, resource: *const Resource) !void { | |
| 557 | // Note: This @intCast is only safe if we assume that the resource was parsed from a .res file, | |
| 558 | // since the maximum data length for a resource in the .res file format is maxInt(u32). | |
| 559 | // TODO: Either codify this properly or use std.math.cast and return an error. | |
| 560 | const data_len: u32 = @intCast(resource.data.len); | |
| 561 | const data_len_including_padding: u32 = std.math.cast(u32, std.mem.alignForward(u33, data_len, 8)) orelse { | |
| 562 | return error.ResourceDataTooLong; | |
| 563 | }; | |
| 564 | // TODO: Verify that this corresponds to an actual PE/COFF limitation for resource data | |
| 565 | // in the final linked binary. The limit may turn out to be shorter than u32 max if both | |
| 566 | // the tree data and the resource data lengths together need to fit within a u32, | |
| 567 | // or it may be longer in which case we would want to add more .rsrc$NN sections | |
| 568 | // to the object file for the data that overflows .rsrc$02. | |
| 569 | self.rsrc02_len = std.math.add(u32, self.rsrc02_len, data_len_including_padding) catch { | |
| 570 | return error.TotalResourceDataTooLong; | |
| 571 | }; | |
| 572 | } | |
| 573 | ||
| 574 | const Lengths = struct { | |
| 575 | level1: u32, | |
| 576 | level2: u32, | |
| 577 | level3: u32, | |
| 578 | data_entries: u32, | |
| 579 | strings: u32, | |
| 580 | padding: u32, | |
| 581 | ||
| 582 | rsrc01: u32, | |
| 583 | rsrc02: u32, | |
| 584 | ||
| 585 | fn stringsStart(self: Lengths) u32 { | |
| 586 | return self.rsrc01 - self.strings - self.padding; | |
| 587 | } | |
| 588 | }; | |
| 589 | ||
| 590 | pub fn dataLengths(self: *const ResourceTree) Lengths { | |
| 591 | var lengths: Lengths = .{ | |
| 592 | .level1 = 0, | |
| 593 | .level2 = 0, | |
| 594 | .level3 = 0, | |
| 595 | .data_entries = 0, | |
| 596 | .strings = 0, | |
| 597 | .padding = 0, | |
| 598 | .rsrc01 = undefined, | |
| 599 | .rsrc02 = self.rsrc02_len, | |
| 600 | }; | |
| 601 | lengths.level1 += @sizeOf(ResourceDirectoryTable); | |
| 602 | for (self.type_to_name_map.values()) |name_to_lang_map| { | |
| 603 | lengths.level1 += @sizeOf(ResourceDirectoryEntry); | |
| 604 | lengths.level2 += @sizeOf(ResourceDirectoryTable); | |
| 605 | for (name_to_lang_map.values()) |lang_to_resources_map| { | |
| 606 | lengths.level2 += @sizeOf(ResourceDirectoryEntry); | |
| 607 | lengths.level3 += @sizeOf(ResourceDirectoryTable); | |
| 608 | for (lang_to_resources_map.values()) |_| { | |
| 609 | lengths.level3 += @sizeOf(ResourceDirectoryEntry); | |
| 610 | lengths.data_entries += @sizeOf(ResourceDataEntry); | |
| 611 | } | |
| 612 | } | |
| 613 | } | |
| 614 | for (self.rsrc_string_table.keys()) |v| { | |
| 615 | lengths.strings += @sizeOf(u16); // string length | |
| 616 | lengths.strings += @intCast(v.name.len * @sizeOf(u16)); | |
| 617 | } | |
| 618 | lengths.rsrc01 = lengths.level1 + lengths.level2 + lengths.level3 + lengths.data_entries + lengths.strings; | |
| 619 | lengths.padding = @intCast((4 -% lengths.rsrc01) % 4); | |
| 620 | lengths.rsrc01 += lengths.padding; | |
| 621 | return lengths; | |
| 622 | } | |
| 623 | ||
| 624 | pub fn sort(self: *ResourceTree) !void { | |
| 625 | const NameOrOrdinalSortContext = struct { | |
| 626 | keys: []NameOrOrdinal, | |
| 627 | ||
| 628 | pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool { | |
| 629 | const a = ctx.keys[a_index]; | |
| 630 | const b = ctx.keys[b_index]; | |
| 631 | if (std.meta.activeTag(a) != std.meta.activeTag(b)) { | |
| 632 | return if (a == .name) true else false; | |
| 633 | } | |
| 634 | switch (a) { | |
| 635 | .name => { | |
| 636 | const n = @min(a.name.len, b.name.len); | |
| 637 | for (a.name[0..n], b.name[0..n]) |a_c, b_c| { | |
| 638 | switch (std.math.order(std.mem.littleToNative(u16, a_c), std.mem.littleToNative(u16, b_c))) { | |
| 639 | .eq => continue, | |
| 640 | .lt => return true, | |
| 641 | .gt => return false, | |
| 642 | } | |
| 643 | } | |
| 644 | return a.name.len < b.name.len; | |
| 645 | }, | |
| 646 | .ordinal => { | |
| 647 | return a.ordinal < b.ordinal; | |
| 648 | }, | |
| 649 | } | |
| 650 | } | |
| 651 | }; | |
| 652 | self.type_to_name_map.sortUnstable(NameOrOrdinalSortContext{ .keys = self.type_to_name_map.keys() }); | |
| 653 | for (self.type_to_name_map.values()) |*name_to_lang_map| { | |
| 654 | name_to_lang_map.sortUnstable(NameOrOrdinalSortContext{ .keys = name_to_lang_map.keys() }); | |
| 655 | } | |
| 656 | const LangSortContext = struct { | |
| 657 | keys: []Language, | |
| 658 | ||
| 659 | pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool { | |
| 660 | return @as(u16, @bitCast(ctx.keys[a_index])) < @as(u16, @bitCast(ctx.keys[b_index])); | |
| 661 | } | |
| 662 | }; | |
| 663 | for (self.type_to_name_map.values()) |*name_to_lang_map| { | |
| 664 | for (name_to_lang_map.values()) |*lang_to_resource_map| { | |
| 665 | lang_to_resource_map.sortUnstable(LangSortContext{ .keys = lang_to_resource_map.keys() }); | |
| 666 | } | |
| 667 | } | |
| 668 | } | |
| 669 | ||
| 670 | pub fn writeCoff( | |
| 671 | self: *const ResourceTree, | |
| 672 | allocator: Allocator, | |
| 673 | writer: anytype, | |
| 674 | resources_in_data_order: []const Resource, | |
| 675 | lengths: Lengths, | |
| 676 | coff_string_table: *StringTable, | |
| 677 | ) ![]const std.coff.Symbol { | |
| 678 | if (self.type_to_name_map.count() == 0) { | |
| 679 | try writer.writeByteNTimes(0, 16); | |
| 680 | return &.{}; | |
| 681 | } | |
| 682 | ||
| 683 | var counting_writer = std.io.countingWriter(writer); | |
| 684 | const w = counting_writer.writer(); | |
| 685 | ||
| 686 | var level2_list: std.ArrayListUnmanaged(*const NameToLanguageMap) = .empty; | |
| 687 | defer level2_list.deinit(allocator); | |
| 688 | ||
| 689 | var level3_list: std.ArrayListUnmanaged(*const LanguageToResourceMap) = .empty; | |
| 690 | defer level3_list.deinit(allocator); | |
| 691 | ||
| 692 | var resources_list: std.ArrayListUnmanaged(*const RelocatableResource) = .empty; | |
| 693 | defer resources_list.deinit(allocator); | |
| 694 | ||
| 695 | var relocations = Relocations.init(allocator); | |
| 696 | defer relocations.deinit(); | |
| 697 | ||
| 698 | var string_offsets = try allocator.alloc(u31, self.rsrc_string_table.count()); | |
| 699 | const strings_start = lengths.stringsStart(); | |
| 700 | defer allocator.free(string_offsets); | |
| 701 | { | |
| 702 | var string_address: u31 = @intCast(strings_start); | |
| 703 | for (self.rsrc_string_table.keys(), 0..) |v, i| { | |
| 704 | string_offsets[i] = string_address; | |
| 705 | string_address += @sizeOf(u16) + @as(u31, @intCast(v.name.len * @sizeOf(u16))); | |
| 706 | } | |
| 707 | } | |
| 708 | ||
| 709 | const level2_start = lengths.level1; | |
| 710 | var level2_address = level2_start; | |
| 711 | { | |
| 712 | const counts = entryTypeCounts(self.type_to_name_map.keys()); | |
| 713 | const table = ResourceDirectoryTable{ | |
| 714 | .characteristics = 0, | |
| 715 | .timestamp = 0, | |
| 716 | .major_version = 0, | |
| 717 | .minor_version = 0, | |
| 718 | .number_of_id_entries = counts.ids, | |
| 719 | .number_of_name_entries = counts.names, | |
| 720 | }; | |
| 721 | try w.writeStructEndian(table, .little); | |
| 722 | ||
| 723 | var it = self.type_to_name_map.iterator(); | |
| 724 | while (it.next()) |entry| { | |
| 725 | const type_value = entry.key_ptr; | |
| 726 | const dir_entry = ResourceDirectoryEntry{ | |
| 727 | .entry = switch (type_value.*) { | |
| 728 | .name => .{ .name_offset = .{ .address = string_offsets[self.rsrc_string_table.getIndex(type_value.*).?] } }, | |
| 729 | .ordinal => .{ .integer_id = type_value.ordinal }, | |
| 730 | }, | |
| 731 | .offset = .{ | |
| 732 | .address = @intCast(level2_address), | |
| 733 | .to_subdirectory = true, | |
| 734 | }, | |
| 735 | }; | |
| 736 | try dir_entry.writeCoff(w); | |
| 737 | level2_address += @sizeOf(ResourceDirectoryTable) + @as(u32, @intCast(entry.value_ptr.count() * @sizeOf(ResourceDirectoryEntry))); | |
| 738 | ||
| 739 | const name_to_lang_map = entry.value_ptr; | |
| 740 | try level2_list.append(allocator, name_to_lang_map); | |
| 741 | } | |
| 742 | } | |
| 743 | std.debug.assert(counting_writer.bytes_written == level2_start); | |
| 744 | ||
| 745 | const level3_start = level2_start + lengths.level2; | |
| 746 | var level3_address = level3_start; | |
| 747 | for (level2_list.items) |name_to_lang_map| { | |
| 748 | const counts = entryTypeCounts(name_to_lang_map.keys()); | |
| 749 | const table = ResourceDirectoryTable{ | |
| 750 | .characteristics = 0, | |
| 751 | .timestamp = 0, | |
| 752 | .major_version = 0, | |
| 753 | .minor_version = 0, | |
| 754 | .number_of_id_entries = counts.ids, | |
| 755 | .number_of_name_entries = counts.names, | |
| 756 | }; | |
| 757 | try w.writeStructEndian(table, .little); | |
| 758 | ||
| 759 | var it = name_to_lang_map.iterator(); | |
| 760 | while (it.next()) |entry| { | |
| 761 | const name_value = entry.key_ptr; | |
| 762 | const dir_entry = ResourceDirectoryEntry{ | |
| 763 | .entry = switch (name_value.*) { | |
| 764 | .name => .{ .name_offset = .{ .address = string_offsets[self.rsrc_string_table.getIndex(name_value.*).?] } }, | |
| 765 | .ordinal => .{ .integer_id = name_value.ordinal }, | |
| 766 | }, | |
| 767 | .offset = .{ | |
| 768 | .address = @intCast(level3_address), | |
| 769 | .to_subdirectory = true, | |
| 770 | }, | |
| 771 | }; | |
| 772 | try dir_entry.writeCoff(w); | |
| 773 | level3_address += @sizeOf(ResourceDirectoryTable) + @as(u32, @intCast(entry.value_ptr.count() * @sizeOf(ResourceDirectoryEntry))); | |
| 774 | ||
| 775 | const lang_to_resources_map = entry.value_ptr; | |
| 776 | try level3_list.append(allocator, lang_to_resources_map); | |
| 777 | } | |
| 778 | } | |
| 779 | std.debug.assert(counting_writer.bytes_written == level3_start); | |
| 780 | ||
| 781 | var reloc_addresses = try allocator.alloc(u32, resources_in_data_order.len); | |
| 782 | defer allocator.free(reloc_addresses); | |
| 783 | ||
| 784 | const data_entries_start = level3_start + lengths.level3; | |
| 785 | var data_entry_address = data_entries_start; | |
| 786 | for (level3_list.items) |lang_to_resources_map| { | |
| 787 | const counts = EntryTypeCounts{ | |
| 788 | .names = 0, | |
| 789 | .ids = @intCast(lang_to_resources_map.count()), | |
| 790 | }; | |
| 791 | const table = ResourceDirectoryTable{ | |
| 792 | .characteristics = 0, | |
| 793 | .timestamp = 0, | |
| 794 | .major_version = 0, | |
| 795 | .minor_version = 0, | |
| 796 | .number_of_id_entries = counts.ids, | |
| 797 | .number_of_name_entries = counts.names, | |
| 798 | }; | |
| 799 | try w.writeStructEndian(table, .little); | |
| 800 | ||
| 801 | var it = lang_to_resources_map.iterator(); | |
| 802 | while (it.next()) |entry| { | |
| 803 | const lang = entry.key_ptr.*; | |
| 804 | const dir_entry = ResourceDirectoryEntry{ | |
| 805 | .entry = .{ .integer_id = lang.asInt() }, | |
| 806 | .offset = .{ | |
| 807 | .address = @intCast(data_entry_address), | |
| 808 | .to_subdirectory = false, | |
| 809 | }, | |
| 810 | }; | |
| 811 | ||
| 812 | const reloc_resource = entry.value_ptr; | |
| 813 | reloc_addresses[reloc_resource.original_index] = @intCast(data_entry_address); | |
| 814 | ||
| 815 | try dir_entry.writeCoff(w); | |
| 816 | data_entry_address += @sizeOf(ResourceDataEntry); | |
| 817 | ||
| 818 | try resources_list.append(allocator, reloc_resource); | |
| 819 | } | |
| 820 | } | |
| 821 | std.debug.assert(counting_writer.bytes_written == data_entries_start); | |
| 822 | ||
| 823 | for (resources_list.items, 0..) |reloc_resource, i| { | |
| 824 | // TODO: This logic works but is convoluted, would be good to clean this up | |
| 825 | const orig_resource = &resources_in_data_order[reloc_resource.original_index]; | |
| 826 | const address: u32 = reloc_addresses[i]; | |
| 827 | try relocations.add(address, self.data_offsets.items[i]); | |
| 828 | const data_entry = ResourceDataEntry{ | |
| 829 | .data_rva = 0, // relocation | |
| 830 | .size = @intCast(orig_resource.data.len), | |
| 831 | .codepage = 0, | |
| 832 | }; | |
| 833 | try w.writeStructEndian(data_entry, .little); | |
| 834 | } | |
| 835 | std.debug.assert(counting_writer.bytes_written == strings_start); | |
| 836 | ||
| 837 | for (self.rsrc_string_table.keys()) |v| { | |
| 838 | const str = v.name; | |
| 839 | try w.writeInt(u16, @intCast(str.len), .little); | |
| 840 | try w.writeAll(std.mem.sliceAsBytes(str)); | |
| 841 | } | |
| 842 | ||
| 843 | try w.writeByteNTimes(0, lengths.padding); | |
| 844 | ||
| 845 | for (relocations.list.items) |relocation| { | |
| 846 | try writeRelocation(w, std.coff.Relocation{ | |
| 847 | .virtual_address = relocation.relocation_address, | |
| 848 | .symbol_table_index = relocation.symbol_index, | |
| 849 | .type = supported_targets.rvaRelocationTypeIndicator(self.coff_options.target).?, | |
| 850 | }); | |
| 851 | } | |
| 852 | ||
| 853 | if (self.coff_options.fold_duplicate_data) { | |
| 854 | for (self.deduplicated_data.keys()) |data| { | |
| 855 | const padding_bytes: u4 = @intCast((8 -% data.len) % 8); | |
| 856 | try w.writeAll(data); | |
| 857 | try w.writeByteNTimes(0, padding_bytes); | |
| 858 | } | |
| 859 | } else { | |
| 860 | for (resources_in_data_order) |resource| { | |
| 861 | const padding_bytes: u4 = @intCast((8 -% resource.data.len) % 8); | |
| 862 | try w.writeAll(resource.data); | |
| 863 | try w.writeByteNTimes(0, padding_bytes); | |
| 864 | } | |
| 865 | } | |
| 866 | ||
| 867 | var symbols = try allocator.alloc(std.coff.Symbol, resources_list.items.len); | |
| 868 | errdefer allocator.free(symbols); | |
| 869 | ||
| 870 | for (relocations.list.items, 0..) |relocation, i| { | |
| 871 | // cvtres.exe writes the symbol names as $R<data offset as hexadecimal>. | |
| 872 | // | |
| 873 | // When the data offset would exceed 6 hex digits in cvtres.exe, it | |
| 874 | // truncates the value down to 6 hex digits. This is bad behavior, since | |
| 875 | // e.g. an initial resource with exactly 16 MiB of data and the | |
| 876 | // resource following it would both have the symbol name $R000000. | |
| 877 | // | |
| 878 | // Instead, if the offset would exceed 6 hexadecimal digits, | |
| 879 | // we put the longer name in the string table. | |
| 880 | // | |
| 881 | // Another option would be to adopt llvm-cvtres' behavior | |
| 882 | // of $R000001, $R000002, etc. rather than using data offset values. | |
| 883 | var name_buf: [8]u8 = undefined; | |
| 884 | if (relocation.data_offset > std.math.maxInt(u24)) { | |
| 885 | const name_slice = try std.fmt.allocPrint(allocator, "$R{X}", .{relocation.data_offset}); | |
| 886 | defer allocator.free(name_slice); | |
| 887 | const string_table_offset: u32 = try coff_string_table.put(allocator, name_slice); | |
| 888 | std.mem.writeInt(u32, name_buf[0..4], 0, .little); | |
| 889 | std.mem.writeInt(u32, name_buf[4..8], string_table_offset, .little); | |
| 890 | } else { | |
| 891 | const name_slice = std.fmt.bufPrint(&name_buf, "$R{X:0>6}", .{relocation.data_offset}) catch unreachable; | |
| 892 | std.debug.assert(name_slice.len == 8); | |
| 893 | } | |
| 894 | ||
| 895 | symbols[i] = .{ | |
| 896 | .name = name_buf, | |
| 897 | .value = relocation.data_offset, | |
| 898 | .section_number = @enumFromInt(2), | |
| 899 | .type = .{ | |
| 900 | .base_type = .NULL, | |
| 901 | .complex_type = .NULL, | |
| 902 | }, | |
| 903 | .storage_class = .STATIC, | |
| 904 | .number_of_aux_symbols = 0, | |
| 905 | }; | |
| 906 | } | |
| 907 | ||
| 908 | return symbols; | |
| 909 | } | |
| 910 | ||
| 911 | fn writeRelocation(writer: anytype, relocation: std.coff.Relocation) !void { | |
| 912 | try writer.writeInt(u32, relocation.virtual_address, .little); | |
| 913 | try writer.writeInt(u32, relocation.symbol_table_index, .little); | |
| 914 | try writer.writeInt(u16, relocation.type, .little); | |
| 915 | } | |
| 916 | ||
| 917 | const EntryTypeCounts = struct { | |
| 918 | names: u16, | |
| 919 | ids: u16, | |
| 920 | }; | |
| 921 | ||
| 922 | fn entryTypeCounts(s: []const NameOrOrdinal) EntryTypeCounts { | |
| 923 | var names: u16 = 0; | |
| 924 | var ordinals: u16 = 0; | |
| 925 | for (s) |v| { | |
| 926 | switch (v) { | |
| 927 | .name => names += 1, | |
| 928 | .ordinal => ordinals += 1, | |
| 929 | } | |
| 930 | } | |
| 931 | return .{ .names = names, .ids = ordinals }; | |
| 932 | } | |
| 933 | }; | |
| 934 | ||
| 935 | const Relocation = struct { | |
| 936 | symbol_index: u32, | |
| 937 | data_offset: u32, | |
| 938 | relocation_address: u32, | |
| 939 | }; | |
| 940 | ||
| 941 | const Relocations = struct { | |
| 942 | allocator: Allocator, | |
| 943 | list: std.ArrayListUnmanaged(Relocation) = .empty, | |
| 944 | cur_symbol_index: u32 = 5, | |
| 945 | ||
| 946 | pub fn init(allocator: Allocator) Relocations { | |
| 947 | return .{ .allocator = allocator }; | |
| 948 | } | |
| 949 | ||
| 950 | pub fn deinit(self: *Relocations) void { | |
| 951 | self.list.deinit(self.allocator); | |
| 952 | } | |
| 953 | ||
| 954 | pub fn add(self: *Relocations, relocation_address: u32, data_offset: u32) !void { | |
| 955 | try self.list.append(self.allocator, .{ | |
| 956 | .symbol_index = self.cur_symbol_index, | |
| 957 | .data_offset = data_offset, | |
| 958 | .relocation_address = relocation_address, | |
| 959 | }); | |
| 960 | self.cur_symbol_index += 1; | |
| 961 | } | |
| 962 | }; | |
| 963 | ||
| 964 | /// Does not do deduplication (only because there's no chance of duplicate strings in this | |
| 965 | /// instance). | |
| 966 | const StringTable = struct { | |
| 967 | bytes: std.ArrayListUnmanaged(u8) = .empty, | |
| 968 | ||
| 969 | pub fn deinit(self: *StringTable, allocator: Allocator) void { | |
| 970 | self.bytes.deinit(allocator); | |
| 971 | } | |
| 972 | ||
| 973 | /// Returns the byte offset of the string in the string table | |
| 974 | pub fn put(self: *StringTable, allocator: Allocator, string: []const u8) !u32 { | |
| 975 | const null_terminated_len = string.len + 1; | |
| 976 | const start_offset = self.totalByteLength(); | |
| 977 | if (start_offset + null_terminated_len > std.math.maxInt(u32)) { | |
| 978 | return error.StringTableOverflow; | |
| 979 | } | |
| 980 | try self.bytes.ensureUnusedCapacity(allocator, null_terminated_len); | |
| 981 | self.bytes.appendSliceAssumeCapacity(string); | |
| 982 | self.bytes.appendAssumeCapacity(0); | |
| 983 | return start_offset; | |
| 984 | } | |
| 985 | ||
| 986 | /// Returns the total byte count of the string table, including the byte count of the size field | |
| 987 | pub fn totalByteLength(self: StringTable) u32 { | |
| 988 | return @intCast(4 + self.bytes.items.len); | |
| 989 | } | |
| 990 | }; | |
| 991 | ||
| 992 | pub const supported_targets = struct { | |
| 993 | /// Enum containing a mixture of names that come from: | |
| 994 | /// - Machine Types constants in the PE format spec: | |
| 995 | /// https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#machine-types | |
| 996 | /// - cvtres.exe /machine options | |
| 997 | /// - Zig/LLVM arch names | |
| 998 | /// All field names are lowercase regardless of their casing used in the above origins. | |
| 999 | pub const Arch = enum { | |
| 1000 | // cvtres.exe /machine names | |
| 1001 | x64, | |
| 1002 | x86, | |
| 1003 | /// Note: Following cvtres.exe's lead, this corresponds to ARMNT, not ARM | |
| 1004 | arm, | |
| 1005 | arm64, | |
| 1006 | arm64ec, | |
| 1007 | arm64x, | |
| 1008 | ia64, | |
| 1009 | ebc, | |
| 1010 | ||
| 1011 | // PE/COFF MACHINE constant names not covered above | |
| 1012 | amd64, | |
| 1013 | i386, | |
| 1014 | armnt, | |
| 1015 | ||
| 1016 | // Zig/LLVM names not already covered above | |
| 1017 | x86_64, | |
| 1018 | aarch64, | |
| 1019 | ||
| 1020 | pub fn toCoffMachineType(arch: Arch) std.coff.MachineType { | |
| 1021 | return switch (arch) { | |
| 1022 | .x64, .amd64, .x86_64 => .X64, | |
| 1023 | .x86, .i386 => .I386, | |
| 1024 | .arm, .armnt => .ARMNT, | |
| 1025 | .arm64, .aarch64 => .ARM64, | |
| 1026 | .arm64ec => .ARM64EC, | |
| 1027 | .arm64x => .ARM64X, | |
| 1028 | .ia64 => .IA64, | |
| 1029 | .ebc => .EBC, | |
| 1030 | }; | |
| 1031 | } | |
| 1032 | ||
| 1033 | pub fn description(arch: Arch) []const u8 { | |
| 1034 | return switch (arch) { | |
| 1035 | .x64, .amd64, .x86_64 => "64-bit X86", | |
| 1036 | .x86, .i386 => "32-bit X86", | |
| 1037 | .arm, .armnt => "ARM Thumb-2 little endian", | |
| 1038 | .arm64, .aarch64 => "ARM64/AArch64 little endian", | |
| 1039 | .arm64ec => "ARM64 \"Emulation Compatible\"", | |
| 1040 | .arm64x => "ARM64 and ARM64EC together", | |
| 1041 | .ia64 => "64-bit Intel Itanium", | |
| 1042 | .ebc => "EFI Byte Code", | |
| 1043 | }; | |
| 1044 | } | |
| 1045 | ||
| 1046 | pub const ordered_for_display: []const Arch = &.{ | |
| 1047 | .x64, | |
| 1048 | .x86_64, | |
| 1049 | .amd64, | |
| 1050 | .x86, | |
| 1051 | .i386, | |
| 1052 | .arm64, | |
| 1053 | .aarch64, | |
| 1054 | .arm, | |
| 1055 | .armnt, | |
| 1056 | .arm64ec, | |
| 1057 | .arm64x, | |
| 1058 | .ia64, | |
| 1059 | .ebc, | |
| 1060 | }; | |
| 1061 | comptime { | |
| 1062 | for (@typeInfo(Arch).@"enum".fields) |enum_field| { | |
| 1063 | _ = std.mem.indexOfScalar(Arch, ordered_for_display, @enumFromInt(enum_field.value)) orelse { | |
| 1064 | @compileError(std.fmt.comptimePrint("'{s}' missing from ordered_for_display", .{enum_field.name})); | |
| 1065 | }; | |
| 1066 | } | |
| 1067 | } | |
| 1068 | ||
| 1069 | pub const longest_name = blk: { | |
| 1070 | var len = 0; | |
| 1071 | for (@typeInfo(Arch).@"enum".fields) |field| { | |
| 1072 | if (field.name.len > len) len = field.name.len; | |
| 1073 | } | |
| 1074 | break :blk len; | |
| 1075 | }; | |
| 1076 | ||
| 1077 | pub fn fromStringIgnoreCase(str: []const u8) ?Arch { | |
| 1078 | if (str.len > longest_name) return null; | |
| 1079 | var lower_buf: [longest_name]u8 = undefined; | |
| 1080 | const lower = std.ascii.lowerString(&lower_buf, str); | |
| 1081 | return std.meta.stringToEnum(Arch, lower); | |
| 1082 | } | |
| 1083 | ||
| 1084 | test fromStringIgnoreCase { | |
| 1085 | try std.testing.expectEqual(.x64, Arch.fromStringIgnoreCase("x64").?); | |
| 1086 | try std.testing.expectEqual(.x64, Arch.fromStringIgnoreCase("X64").?); | |
| 1087 | try std.testing.expectEqual(.aarch64, Arch.fromStringIgnoreCase("Aarch64").?); | |
| 1088 | try std.testing.expectEqual(null, Arch.fromStringIgnoreCase("armzzz")); | |
| 1089 | try std.testing.expectEqual(null, Arch.fromStringIgnoreCase("long string that is longer than any field")); | |
| 1090 | } | |
| 1091 | }; | |
| 1092 | ||
| 1093 | // https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#type-indicators | |
| 1094 | pub fn rvaRelocationTypeIndicator(target: std.coff.MachineType) ?u16 { | |
| 1095 | return switch (target) { | |
| 1096 | .X64 => 0x3, // IMAGE_REL_AMD64_ADDR32NB | |
| 1097 | .I386 => 0x7, // IMAGE_REL_I386_DIR32NB | |
| 1098 | .ARMNT => 0x2, // IMAGE_REL_ARM_ADDR32NB | |
| 1099 | .ARM64, .ARM64EC, .ARM64X => 0x2, // IMAGE_REL_ARM64_ADDR32NB | |
| 1100 | .IA64 => 0x10, // IMAGE_REL_IA64_DIR32NB | |
| 1101 | .EBC => 0x1, // This is what cvtres.exe writes for this target, unsure where it comes from | |
| 1102 | else => null, | |
| 1103 | }; | |
| 1104 | } | |
| 1105 | ||
| 1106 | pub fn isSupported(target: std.coff.MachineType) bool { | |
| 1107 | return rvaRelocationTypeIndicator(target) != null; | |
| 1108 | } | |
| 1109 | ||
| 1110 | comptime { | |
| 1111 | // Enforce two things: | |
| 1112 | // 1. Arch enum field names are all lowercase (necessary for how fromStringIgnoreCase is implemented) | |
| 1113 | // 2. All enum fields in Arch have an associated RVA relocation type when converted to a coff.MachineType | |
| 1114 | for (@typeInfo(Arch).@"enum".fields) |enum_field| { | |
| 1115 | const all_lower = all_lower: for (enum_field.name) |c| { | |
| 1116 | if (std.ascii.isUpper(c)) break :all_lower false; | |
| 1117 | } else break :all_lower true; | |
| 1118 | if (!all_lower) @compileError(std.fmt.comptimePrint("Arch field is not all lowercase: {s}", .{enum_field.name})); | |
| 1119 | const coff_machine = @field(Arch, enum_field.name).toCoffMachineType(); | |
| 1120 | _ = rvaRelocationTypeIndicator(coff_machine) orelse { | |
| 1121 | @compileError(std.fmt.comptimePrint("No RVA relocation for Arch: {s}", .{enum_field.name})); | |
| 1122 | }; | |
| 1123 | } | |
| 1124 | } | |
| 1125 | }; |
lib/compiler/resinator/main.zig+311-93| ... | ... | @@ -7,7 +7,10 @@ const Diagnostics = @import("errors.zig").Diagnostics; |
| 7 | 7 | const cli = @import("cli.zig"); |
| 8 | 8 | const preprocess = @import("preprocess.zig"); |
| 9 | 9 | const renderErrorMessage = @import("utils.zig").renderErrorMessage; |
| 10 | const openFileNotDir = @import("utils.zig").openFileNotDir; | |
| 11 | const cvtres = @import("cvtres.zig"); | |
| 10 | 12 | const hasDisjointCodePage = @import("disjoint_code_page.zig").hasDisjointCodePage; |
| 13 | const fmtResourceType = @import("res.zig").NameOrOrdinal.fmtResourceType; | |
| 11 | 14 | const aro = @import("aro"); |
| 12 | 15 | |
| 13 | 16 | pub fn main() !void { |
| ... | ... | @@ -135,7 +138,10 @@ pub fn main() !void { |
| 135 | 138 | |
| 136 | 139 | try argv.append("arocc"); // dummy command name |
| 137 | 140 | try preprocess.appendAroArgs(aro_arena, &argv, options, include_paths); |
| 138 | try argv.append(options.input_filename); | |
| 141 | try argv.append(switch (options.input_source) { | |
| 142 | .stdio => "-", | |
| 143 | .filename => |filename| filename, | |
| 144 | }); | |
| 139 | 145 | |
| 140 | 146 | if (options.verbose) { |
| 141 | 147 | try stdout_writer.writeAll("Preprocessor: arocc (built-in)\n"); |
| ... | ... | @@ -164,120 +170,332 @@ pub fn main() !void { |
| 164 | 170 | |
| 165 | 171 | break :full_input try preprocessed_buf.toOwnedSlice(); |
| 166 | 172 | } else { |
| 167 | break :full_input std.fs.cwd().readFileAlloc(allocator, options.input_filename, std.math.maxInt(usize)) catch |err| { | |
| 168 | try error_handler.emitMessage(allocator, .err, "unable to read input file path '{s}': {s}", .{ options.input_filename, @errorName(err) }); | |
| 169 | std.process.exit(1); | |
| 170 | }; | |
| 173 | switch (options.input_source) { | |
| 174 | .stdio => |file| { | |
| 175 | break :full_input file.readToEndAlloc(allocator, std.math.maxInt(usize)) catch |err| { | |
| 176 | try error_handler.emitMessage(allocator, .err, "unable to read input from stdin: {s}", .{@errorName(err)}); | |
| 177 | std.process.exit(1); | |
| 178 | }; | |
| 179 | }, | |
| 180 | .filename => |input_filename| { | |
| 181 | break :full_input std.fs.cwd().readFileAlloc(allocator, input_filename, std.math.maxInt(usize)) catch |err| { | |
| 182 | try error_handler.emitMessage(allocator, .err, "unable to read input file path '{s}': {s}", .{ input_filename, @errorName(err) }); | |
| 183 | std.process.exit(1); | |
| 184 | }; | |
| 185 | }, | |
| 186 | } | |
| 171 | 187 | } |
| 172 | 188 | }; |
| 173 | 189 | defer allocator.free(full_input); |
| 174 | 190 | |
| 175 | 191 | if (options.preprocess == .only) { |
| 176 | try std.fs.cwd().writeFile(.{ .sub_path = options.output_filename, .data = full_input }); | |
| 192 | switch (options.output_source) { | |
| 193 | .stdio => |output_file| { | |
| 194 | try output_file.writeAll(full_input); | |
| 195 | }, | |
| 196 | .filename => |output_filename| { | |
| 197 | try std.fs.cwd().writeFile(.{ .sub_path = output_filename, .data = full_input }); | |
| 198 | }, | |
| 199 | } | |
| 177 | 200 | return; |
| 178 | 201 | } |
| 179 | 202 | |
| 180 | // Note: We still want to run this when no-preprocess is set because: | |
| 181 | // 1. We want to print accurate line numbers after removing multiline comments | |
| 182 | // 2. We want to be able to handle an already-preprocessed input with #line commands in it | |
| 183 | var mapping_results = parseAndRemoveLineCommands(allocator, full_input, full_input, .{ .initial_filename = options.input_filename }) catch |err| switch (err) { | |
| 184 | error.InvalidLineCommand => { | |
| 185 | // TODO: Maybe output the invalid line command | |
| 186 | try error_handler.emitMessage(allocator, .err, "invalid line command in the preprocessed source", .{}); | |
| 187 | if (options.preprocess == .no) { | |
| 188 | try error_handler.emitMessage(allocator, .note, "line commands must be of the format: #line <num> \"<path>\"", .{}); | |
| 189 | } else { | |
| 190 | try error_handler.emitMessage(allocator, .note, "this is likely to be a bug, please report it", .{}); | |
| 203 | var resources = resources: { | |
| 204 | const need_intermediate_res = options.output_format == .coff and options.input_format != .res; | |
| 205 | var res_stream = if (need_intermediate_res) | |
| 206 | IoStream{ | |
| 207 | .name = "<in-memory intermediate res>", | |
| 208 | .intermediate = true, | |
| 209 | .source = .{ .memory = .empty }, | |
| 191 | 210 | } |
| 192 | std.process.exit(1); | |
| 193 | }, | |
| 194 | error.LineNumberOverflow => { | |
| 195 | // TODO: Better error message | |
| 196 | try error_handler.emitMessage(allocator, .err, "line number count exceeded maximum of {}", .{std.math.maxInt(usize)}); | |
| 197 | std.process.exit(1); | |
| 198 | }, | |
| 199 | error.OutOfMemory => |e| return e, | |
| 200 | }; | |
| 201 | defer mapping_results.mappings.deinit(allocator); | |
| 211 | else if (options.input_format == .res) | |
| 212 | IoStream.fromIoSource(options.input_source, .input) catch |err| { | |
| 213 | try error_handler.emitMessage(allocator, .err, "unable to read res file path '{s}': {s}", .{ options.input_source.filename, @errorName(err) }); | |
| 214 | std.process.exit(1); | |
| 215 | } | |
| 216 | else | |
| 217 | IoStream.fromIoSource(options.output_source, .output) catch |err| { | |
| 218 | try error_handler.emitMessage(allocator, .err, "unable to create output file '{s}': {s}", .{ options.output_source.filename, @errorName(err) }); | |
| 219 | std.process.exit(1); | |
| 220 | }; | |
| 221 | defer res_stream.deinit(allocator); | |
| 222 | ||
| 223 | const res_data = res_data: { | |
| 224 | if (options.input_format != .res) { | |
| 225 | // Note: We still want to run this when no-preprocess is set because: | |
| 226 | // 1. We want to print accurate line numbers after removing multiline comments | |
| 227 | // 2. We want to be able to handle an already-preprocessed input with #line commands in it | |
| 228 | var mapping_results = parseAndRemoveLineCommands(allocator, full_input, full_input, .{ .initial_filename = options.input_source.filename }) catch |err| switch (err) { | |
| 229 | error.InvalidLineCommand => { | |
| 230 | // TODO: Maybe output the invalid line command | |
| 231 | try error_handler.emitMessage(allocator, .err, "invalid line command in the preprocessed source", .{}); | |
| 232 | if (options.preprocess == .no) { | |
| 233 | try error_handler.emitMessage(allocator, .note, "line commands must be of the format: #line <num> \"<path>\"", .{}); | |
| 234 | } else { | |
| 235 | try error_handler.emitMessage(allocator, .note, "this is likely to be a bug, please report it", .{}); | |
| 236 | } | |
| 237 | std.process.exit(1); | |
| 238 | }, | |
| 239 | error.LineNumberOverflow => { | |
| 240 | // TODO: Better error message | |
| 241 | try error_handler.emitMessage(allocator, .err, "line number count exceeded maximum of {}", .{std.math.maxInt(usize)}); | |
| 242 | std.process.exit(1); | |
| 243 | }, | |
| 244 | error.OutOfMemory => |e| return e, | |
| 245 | }; | |
| 246 | defer mapping_results.mappings.deinit(allocator); | |
| 247 | ||
| 248 | const default_code_page = options.default_code_page orelse .windows1252; | |
| 249 | const has_disjoint_code_page = hasDisjointCodePage(mapping_results.result, &mapping_results.mappings, default_code_page); | |
| 250 | ||
| 251 | const final_input = try removeComments(mapping_results.result, mapping_results.result, &mapping_results.mappings); | |
| 252 | ||
| 253 | var diagnostics = Diagnostics.init(allocator); | |
| 254 | defer diagnostics.deinit(); | |
| 255 | ||
| 256 | const res_stream_writer = res_stream.source.writer(allocator); | |
| 257 | var output_buffered_stream = std.io.bufferedWriter(res_stream_writer); | |
| 258 | ||
| 259 | compile(allocator, final_input, output_buffered_stream.writer(), .{ | |
| 260 | .cwd = std.fs.cwd(), | |
| 261 | .diagnostics = &diagnostics, | |
| 262 | .source_mappings = &mapping_results.mappings, | |
| 263 | .dependencies_list = maybe_dependencies_list, | |
| 264 | .ignore_include_env_var = options.ignore_include_env_var, | |
| 265 | .extra_include_paths = options.extra_include_paths.items, | |
| 266 | .system_include_paths = include_paths, | |
| 267 | .default_language_id = options.default_language_id, | |
| 268 | .default_code_page = default_code_page, | |
| 269 | .disjoint_code_page = has_disjoint_code_page, | |
| 270 | .verbose = options.verbose, | |
| 271 | .null_terminate_string_table_strings = options.null_terminate_string_table_strings, | |
| 272 | .max_string_literal_codepoints = options.max_string_literal_codepoints, | |
| 273 | .silent_duplicate_control_ids = options.silent_duplicate_control_ids, | |
| 274 | .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page, | |
| 275 | }) catch |err| switch (err) { | |
| 276 | error.ParseError, error.CompileError => { | |
| 277 | try error_handler.emitDiagnostics(allocator, std.fs.cwd(), final_input, &diagnostics, mapping_results.mappings); | |
| 278 | // Delete the output file on error | |
| 279 | res_stream.cleanupAfterError(); | |
| 280 | std.process.exit(1); | |
| 281 | }, | |
| 282 | else => |e| return e, | |
| 283 | }; | |
| 202 | 284 | |
| 203 | const default_code_page = options.default_code_page orelse .windows1252; | |
| 204 | const has_disjoint_code_page = hasDisjointCodePage(mapping_results.result, &mapping_results.mappings, default_code_page); | |
| 285 | try output_buffered_stream.flush(); | |
| 205 | 286 | |
| 206 | const final_input = try removeComments(mapping_results.result, mapping_results.result, &mapping_results.mappings); | |
| 287 | // print any warnings/notes | |
| 288 | if (!zig_integration) { | |
| 289 | diagnostics.renderToStdErr(std.fs.cwd(), final_input, stderr_config, mapping_results.mappings); | |
| 290 | } | |
| 207 | 291 | |
| 208 | var output_file = std.fs.cwd().createFile(options.output_filename, .{}) catch |err| { | |
| 209 | try error_handler.emitMessage(allocator, .err, "unable to create output file '{s}': {s}", .{ options.output_filename, @errorName(err) }); | |
| 292 | // write the depfile | |
| 293 | if (options.depfile_path) |depfile_path| { | |
| 294 | var depfile = std.fs.cwd().createFile(depfile_path, .{}) catch |err| { | |
| 295 | try error_handler.emitMessage(allocator, .err, "unable to create depfile '{s}': {s}", .{ depfile_path, @errorName(err) }); | |
| 296 | std.process.exit(1); | |
| 297 | }; | |
| 298 | defer depfile.close(); | |
| 299 | ||
| 300 | const depfile_writer = depfile.writer(); | |
| 301 | var depfile_buffered_writer = std.io.bufferedWriter(depfile_writer); | |
| 302 | switch (options.depfile_fmt) { | |
| 303 | .json => { | |
| 304 | var write_stream = std.json.writeStream(depfile_buffered_writer.writer(), .{ .whitespace = .indent_2 }); | |
| 305 | defer write_stream.deinit(); | |
| 306 | ||
| 307 | try write_stream.beginArray(); | |
| 308 | for (dependencies_list.items) |dep_path| { | |
| 309 | try write_stream.write(dep_path); | |
| 310 | } | |
| 311 | try write_stream.endArray(); | |
| 312 | }, | |
| 313 | } | |
| 314 | try depfile_buffered_writer.flush(); | |
| 315 | } | |
| 316 | } | |
| 317 | ||
| 318 | if (options.output_format != .coff) return; | |
| 319 | ||
| 320 | break :res_data res_stream.source.readAll(allocator) catch |err| { | |
| 321 | try error_handler.emitMessage(allocator, .err, "unable to read res from '{s}': {s}", .{ res_stream.name, @errorName(err) }); | |
| 322 | std.process.exit(1); | |
| 323 | }; | |
| 324 | }; | |
| 325 | // No need to keep the res_data around after parsing the resources from it | |
| 326 | defer res_data.deinit(allocator); | |
| 327 | ||
| 328 | std.debug.assert(options.output_format == .coff); | |
| 329 | ||
| 330 | // TODO: Maybe use a buffered file reader instead of reading file into memory -> fbs | |
| 331 | var fbs = std.io.fixedBufferStream(res_data.bytes); | |
| 332 | break :resources cvtres.parseRes(allocator, fbs.reader(), .{ .max_size = res_data.bytes.len }) catch |err| { | |
| 333 | // TODO: Better errors | |
| 334 | try error_handler.emitMessage(allocator, .err, "unable to parse res from '{s}': {s}", .{ res_stream.name, @errorName(err) }); | |
| 335 | std.process.exit(1); | |
| 336 | }; | |
| 337 | }; | |
| 338 | defer resources.deinit(); | |
| 339 | ||
| 340 | var coff_stream = IoStream.fromIoSource(options.output_source, .output) catch |err| { | |
| 341 | try error_handler.emitMessage(allocator, .err, "unable to create output file '{s}': {s}", .{ options.output_source.filename, @errorName(err) }); | |
| 210 | 342 | std.process.exit(1); |
| 211 | 343 | }; |
| 212 | var output_file_closed = false; | |
| 213 | defer if (!output_file_closed) output_file.close(); | |
| 214 | ||
| 215 | var diagnostics = Diagnostics.init(allocator); | |
| 216 | defer diagnostics.deinit(); | |
| 217 | ||
| 218 | var output_buffered_stream = std.io.bufferedWriter(output_file.writer()); | |
| 219 | ||
| 220 | compile(allocator, final_input, output_buffered_stream.writer(), .{ | |
| 221 | .cwd = std.fs.cwd(), | |
| 222 | .diagnostics = &diagnostics, | |
| 223 | .source_mappings = &mapping_results.mappings, | |
| 224 | .dependencies_list = maybe_dependencies_list, | |
| 225 | .ignore_include_env_var = options.ignore_include_env_var, | |
| 226 | .extra_include_paths = options.extra_include_paths.items, | |
| 227 | .system_include_paths = include_paths, | |
| 228 | .default_language_id = options.default_language_id, | |
| 229 | .default_code_page = default_code_page, | |
| 230 | .disjoint_code_page = has_disjoint_code_page, | |
| 231 | .verbose = options.verbose, | |
| 232 | .null_terminate_string_table_strings = options.null_terminate_string_table_strings, | |
| 233 | .max_string_literal_codepoints = options.max_string_literal_codepoints, | |
| 234 | .silent_duplicate_control_ids = options.silent_duplicate_control_ids, | |
| 235 | .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page, | |
| 236 | }) catch |err| switch (err) { | |
| 237 | error.ParseError, error.CompileError => { | |
| 238 | try error_handler.emitDiagnostics(allocator, std.fs.cwd(), final_input, &diagnostics, mapping_results.mappings); | |
| 239 | // Delete the output file on error | |
| 240 | output_file.close(); | |
| 241 | output_file_closed = true; | |
| 242 | // Failing to delete is not really a big deal, so swallow any errors | |
| 243 | std.fs.cwd().deleteFile(options.output_filename) catch {}; | |
| 244 | std.process.exit(1); | |
| 245 | }, | |
| 246 | else => |e| return e, | |
| 344 | defer coff_stream.deinit(allocator); | |
| 345 | ||
| 346 | var coff_output_buffered_stream = std.io.bufferedWriter(coff_stream.source.writer(allocator)); | |
| 347 | ||
| 348 | var cvtres_diagnostics: cvtres.Diagnostics = .{ .none = {} }; | |
| 349 | cvtres.writeCoff(allocator, coff_output_buffered_stream.writer(), resources.list.items, options.coff_options, &cvtres_diagnostics) catch |err| { | |
| 350 | switch (err) { | |
| 351 | error.DuplicateResource => { | |
| 352 | const duplicate_resource = resources.list.items[cvtres_diagnostics.duplicate_resource]; | |
| 353 | try error_handler.emitMessage(allocator, .err, "duplicate resource [id: {}, type: {}, language: {}]", .{ | |
| 354 | duplicate_resource.name_value, | |
| 355 | fmtResourceType(duplicate_resource.type_value), | |
| 356 | duplicate_resource.language, | |
| 357 | }); | |
| 358 | }, | |
| 359 | error.ResourceDataTooLong => { | |
| 360 | const overflow_resource = resources.list.items[cvtres_diagnostics.duplicate_resource]; | |
| 361 | try error_handler.emitMessage(allocator, .err, "resource has a data length that is too large to be written into a coff section", .{}); | |
| 362 | try error_handler.emitMessage(allocator, .note, "the resource with the invalid size is [id: {}, type: {}, language: {}]", .{ | |
| 363 | overflow_resource.name_value, | |
| 364 | fmtResourceType(overflow_resource.type_value), | |
| 365 | overflow_resource.language, | |
| 366 | }); | |
| 367 | }, | |
| 368 | error.TotalResourceDataTooLong => { | |
| 369 | const overflow_resource = resources.list.items[cvtres_diagnostics.duplicate_resource]; | |
| 370 | try error_handler.emitMessage(allocator, .err, "total resource data exceeds the maximum of the coff 'size of raw data' field", .{}); | |
| 371 | try error_handler.emitMessage(allocator, .note, "size overflow occurred when attempting to write this resource: [id: {}, type: {}, language: {}]", .{ | |
| 372 | overflow_resource.name_value, | |
| 373 | fmtResourceType(overflow_resource.type_value), | |
| 374 | overflow_resource.language, | |
| 375 | }); | |
| 376 | }, | |
| 377 | else => { | |
| 378 | try error_handler.emitMessage(allocator, .err, "unable to write coff output file '{s}': {s}", .{ coff_stream.name, @errorName(err) }); | |
| 379 | }, | |
| 380 | } | |
| 381 | // Delete the output file on error | |
| 382 | coff_stream.cleanupAfterError(); | |
| 383 | std.process.exit(1); | |
| 247 | 384 | }; |
| 248 | 385 | |
| 249 | try output_buffered_stream.flush(); | |
| 386 | try coff_output_buffered_stream.flush(); | |
| 387 | } | |
| 250 | 388 | |
| 251 | // print any warnings/notes | |
| 252 | if (!zig_integration) { | |
| 253 | diagnostics.renderToStdErr(std.fs.cwd(), final_input, stderr_config, mapping_results.mappings); | |
| 254 | } | |
| 389 | const IoStream = struct { | |
| 390 | name: []const u8, | |
| 391 | intermediate: bool, | |
| 392 | source: Source, | |
| 255 | 393 | |
| 256 | // write the depfile | |
| 257 | if (options.depfile_path) |depfile_path| { | |
| 258 | var depfile = std.fs.cwd().createFile(depfile_path, .{}) catch |err| { | |
| 259 | try error_handler.emitMessage(allocator, .err, "unable to create depfile '{s}': {s}", .{ depfile_path, @errorName(err) }); | |
| 260 | std.process.exit(1); | |
| 394 | pub const IoDirection = enum { input, output }; | |
| 395 | ||
| 396 | pub fn fromIoSource(source: cli.Options.IoSource, io: IoDirection) !IoStream { | |
| 397 | return .{ | |
| 398 | .name = switch (source) { | |
| 399 | .filename => |filename| filename, | |
| 400 | .stdio => switch (io) { | |
| 401 | .input => "<stdin>", | |
| 402 | .output => "<stdout>", | |
| 403 | }, | |
| 404 | }, | |
| 405 | .intermediate = false, | |
| 406 | .source = try Source.fromIoSource(source, io), | |
| 261 | 407 | }; |
| 262 | defer depfile.close(); | |
| 263 | ||
| 264 | const depfile_writer = depfile.writer(); | |
| 265 | var depfile_buffered_writer = std.io.bufferedWriter(depfile_writer); | |
| 266 | switch (options.depfile_fmt) { | |
| 267 | .json => { | |
| 268 | var write_stream = std.json.writeStream(depfile_buffered_writer.writer(), .{ .whitespace = .indent_2 }); | |
| 269 | defer write_stream.deinit(); | |
| 270 | ||
| 271 | try write_stream.beginArray(); | |
| 272 | for (dependencies_list.items) |dep_path| { | |
| 273 | try write_stream.write(dep_path); | |
| 274 | } | |
| 275 | try write_stream.endArray(); | |
| 408 | } | |
| 409 | ||
| 410 | pub fn deinit(self: *IoStream, allocator: std.mem.Allocator) void { | |
| 411 | self.source.deinit(allocator); | |
| 412 | } | |
| 413 | ||
| 414 | pub fn cleanupAfterError(self: *IoStream) void { | |
| 415 | switch (self.source) { | |
| 416 | .file => |file| { | |
| 417 | // Delete the output file on error | |
| 418 | file.close(); | |
| 419 | // Failing to delete is not really a big deal, so swallow any errors | |
| 420 | std.fs.cwd().deleteFile(self.name) catch {}; | |
| 276 | 421 | }, |
| 422 | .stdio, .memory, .closed => return, | |
| 277 | 423 | } |
| 278 | try depfile_buffered_writer.flush(); | |
| 279 | 424 | } |
| 280 | } | |
| 425 | ||
| 426 | pub const Source = union(enum) { | |
| 427 | file: std.fs.File, | |
| 428 | stdio: std.fs.File, | |
| 429 | memory: std.ArrayListUnmanaged(u8), | |
| 430 | /// The source has been closed and any usage of the Source in this state is illegal (except deinit). | |
| 431 | closed: void, | |
| 432 | ||
| 433 | pub fn fromIoSource(source: cli.Options.IoSource, io: IoDirection) !Source { | |
| 434 | switch (source) { | |
| 435 | .filename => |filename| return .{ | |
| 436 | .file = switch (io) { | |
| 437 | .input => try openFileNotDir(std.fs.cwd(), filename, .{}), | |
| 438 | .output => try std.fs.cwd().createFile(filename, .{}), | |
| 439 | }, | |
| 440 | }, | |
| 441 | .stdio => |file| return .{ .stdio = file }, | |
| 442 | } | |
| 443 | } | |
| 444 | ||
| 445 | pub fn deinit(self: *Source, allocator: std.mem.Allocator) void { | |
| 446 | switch (self.*) { | |
| 447 | .file => |file| file.close(), | |
| 448 | .stdio => {}, | |
| 449 | .memory => |*list| list.deinit(allocator), | |
| 450 | .closed => {}, | |
| 451 | } | |
| 452 | } | |
| 453 | ||
| 454 | pub const Data = struct { | |
| 455 | bytes: []const u8, | |
| 456 | needs_free: bool, | |
| 457 | ||
| 458 | pub fn deinit(self: Data, allocator: std.mem.Allocator) void { | |
| 459 | if (self.needs_free) { | |
| 460 | allocator.free(self.bytes); | |
| 461 | } | |
| 462 | } | |
| 463 | }; | |
| 464 | ||
| 465 | pub fn readAll(self: Source, allocator: std.mem.Allocator) !Data { | |
| 466 | return switch (self) { | |
| 467 | inline .file, .stdio => |file| .{ | |
| 468 | .bytes = try file.readToEndAlloc(allocator, std.math.maxInt(usize)), | |
| 469 | .needs_free = true, | |
| 470 | }, | |
| 471 | .memory => |list| .{ .bytes = list.items, .needs_free = false }, | |
| 472 | .closed => unreachable, | |
| 473 | }; | |
| 474 | } | |
| 475 | ||
| 476 | pub const WriterContext = struct { | |
| 477 | self: *Source, | |
| 478 | allocator: std.mem.Allocator, | |
| 479 | }; | |
| 480 | pub const WriteError = std.mem.Allocator.Error || std.fs.File.WriteError; | |
| 481 | pub const Writer = std.io.Writer(WriterContext, WriteError, write); | |
| 482 | ||
| 483 | pub fn write(ctx: WriterContext, bytes: []const u8) WriteError!usize { | |
| 484 | switch (ctx.self.*) { | |
| 485 | inline .file, .stdio => |file| return file.write(bytes), | |
| 486 | .memory => |*list| { | |
| 487 | try list.appendSlice(ctx.allocator, bytes); | |
| 488 | return bytes.len; | |
| 489 | }, | |
| 490 | .closed => unreachable, | |
| 491 | } | |
| 492 | } | |
| 493 | ||
| 494 | pub fn writer(self: *Source, allocator: std.mem.Allocator) Writer { | |
| 495 | return .{ .context = .{ .self = self, .allocator = allocator } }; | |
| 496 | } | |
| 497 | }; | |
| 498 | }; | |
| 281 | 499 | |
| 282 | 500 | fn getIncludePaths(arena: std.mem.Allocator, auto_includes_option: cli.Options.AutoIncludes, zig_lib_dir: []const u8) ![]const []const u8 { |
| 283 | 501 | var includes = auto_includes_option; |