| 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,6 +5,7 @@ const lang = @import("lang.zig"); |
| 5 | const res = @import("res.zig"); | 5 | const res = @import("res.zig"); |
| 6 | const Allocator = std.mem.Allocator; | 6 | const Allocator = std.mem.Allocator; |
| 7 | const lex = @import("lex.zig"); | 7 | const lex = @import("lex.zig"); |
| 8 | const cvtres = @import("cvtres.zig"); | ||
| 8 | 9 | ||
| 9 | /// This is what /SL 100 will set the maximum string literal length to | 10 | /// This is what /SL 100 will set the maximum string literal length to |
| 10 | pub const max_string_literal_length_100_percent = 8192; | 11 | pub const max_string_literal_length_100_percent = 8192; |
| ... | @@ -59,6 +60,20 @@ pub const usage_string_after_command_name = | ... | @@ -59,6 +60,20 @@ pub const usage_string_after_command_name = |
| 59 | \\ the .rc includes or otherwise depends on. | 60 | \\ the .rc includes or otherwise depends on. |
| 60 | \\ /:depfile-fmt <value> Output format of the depfile, if /:depfile is set. | 61 | \\ /:depfile-fmt <value> Output format of the depfile, if /:depfile is set. |
| 61 | \\ json (default) A top-level JSON array of paths | 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 | \\Note: For compatibility reasons, all custom options start with : | 78 | \\Note: For compatibility reasons, all custom options start with : |
| 64 | \\ | 79 | \\ |
| ... | @@ -131,8 +146,8 @@ pub const Diagnostics = struct { | ... | @@ -131,8 +146,8 @@ pub const Diagnostics = struct { |
| 131 | 146 | ||
| 132 | pub const Options = struct { | 147 | pub const Options = struct { |
| 133 | allocator: Allocator, | 148 | allocator: Allocator, |
| 134 | input_filename: []const u8 = &[_]u8{}, | 149 | input_source: IoSource = .{ .filename = &[_]u8{} }, |
| 135 | output_filename: []const u8 = &[_]u8{}, | 150 | output_source: IoSource = .{ .filename = &[_]u8{} }, |
| 136 | extra_include_paths: std.ArrayListUnmanaged([]const u8) = .empty, | 151 | extra_include_paths: std.ArrayListUnmanaged([]const u8) = .empty, |
| 137 | ignore_include_env_var: bool = false, | 152 | ignore_include_env_var: bool = false, |
| 138 | preprocess: Preprocess = .yes, | 153 | preprocess: Preprocess = .yes, |
| ... | @@ -149,9 +164,30 @@ pub const Options = struct { | ... | @@ -149,9 +164,30 @@ pub const Options = struct { |
| 149 | auto_includes: AutoIncludes = .any, | 164 | auto_includes: AutoIncludes = .any, |
| 150 | depfile_path: ?[]const u8 = null, | 165 | depfile_path: ?[]const u8 = null, |
| 151 | depfile_fmt: DepfileFormat = .json, | 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 | pub const AutoIncludes = enum { any, msvc, gnu, none }; | 175 | pub const AutoIncludes = enum { any, msvc, gnu, none }; |
| 154 | pub const DepfileFormat = enum { json }; | 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 | pub const Preprocess = enum { no, yes, only }; | 191 | pub const Preprocess = enum { no, yes, only }; |
| 156 | pub const SymbolAction = enum { define, undefine }; | 192 | pub const SymbolAction = enum { define, undefine }; |
| 157 | pub const SymbolValue = union(SymbolAction) { | 193 | pub const SymbolValue = union(SymbolAction) { |
| ... | @@ -198,9 +234,10 @@ pub const Options = struct { | ... | @@ -198,9 +234,10 @@ pub const Options = struct { |
| 198 | try self.symbols.put(self.allocator, duped_key, .{ .undefine = {} }); | 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 | /// - does not have an extension, and | 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 | /// then this function will append `.rc` to the input filename | 241 | /// then this function will append `.rc` to the input filename |
| 205 | /// | 242 | /// |
| 206 | /// Note: This behavior is different from the Win32 compiler. | 243 | /// Note: This behavior is different from the Win32 compiler. |
| ... | @@ -213,14 +250,18 @@ pub const Options = struct { | ... | @@ -213,14 +250,18 @@ pub const Options = struct { |
| 213 | /// of the .rc extension being omitted from the CLI args, but still | 250 | /// of the .rc extension being omitted from the CLI args, but still |
| 214 | /// work fine if the file itself does not have an extension. | 251 | /// work fine if the file itself does not have an extension. |
| 215 | pub fn maybeAppendRC(options: *Options, cwd: std.fs.Dir) !void { | 252 | pub fn maybeAppendRC(options: *Options, cwd: std.fs.Dir) !void { |
| 216 | if (std.fs.path.extension(options.input_filename).len == 0) { | 253 | switch (options.input_source) { |
| 217 | cwd.access(options.input_filename, .{}) catch |err| switch (err) { | 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 | error.FileNotFound => { | 259 | error.FileNotFound => { |
| 219 | var filename_bytes = try options.allocator.alloc(u8, options.input_filename.len + 3); | 260 | var filename_bytes = try options.allocator.alloc(u8, options.input_source.filename.len + 3); |
| 220 | @memcpy(filename_bytes[0..options.input_filename.len], options.input_filename); | 261 | @memcpy(filename_bytes[0..options.input_source.filename.len], options.input_source.filename); |
| 221 | @memcpy(filename_bytes[filename_bytes.len - 3 ..], ".rc"); | 262 | @memcpy(filename_bytes[filename_bytes.len - 3 ..], ".rc"); |
| 222 | options.allocator.free(options.input_filename); | 263 | options.allocator.free(options.input_source.filename); |
| 223 | options.input_filename = filename_bytes; | 264 | options.input_source = .{ .filename = filename_bytes }; |
| 224 | }, | 265 | }, |
| 225 | else => {}, | 266 | else => {}, |
| 226 | }; | 267 | }; |
| ... | @@ -232,8 +273,14 @@ pub const Options = struct { | ... | @@ -232,8 +273,14 @@ pub const Options = struct { |
| 232 | self.allocator.free(extra_include_path); | 273 | self.allocator.free(extra_include_path); |
| 233 | } | 274 | } |
| 234 | self.extra_include_paths.deinit(self.allocator); | 275 | self.extra_include_paths.deinit(self.allocator); |
| 235 | self.allocator.free(self.input_filename); | 276 | switch (self.input_source) { |
| 236 | self.allocator.free(self.output_filename); | 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 | var symbol_it = self.symbols.iterator(); | 284 | var symbol_it = self.symbols.iterator(); |
| 238 | while (symbol_it.next()) |entry| { | 285 | while (symbol_it.next()) |entry| { |
| 239 | self.allocator.free(entry.key_ptr.*); | 286 | self.allocator.free(entry.key_ptr.*); |
| ... | @@ -243,11 +290,26 @@ pub const Options = struct { | ... | @@ -243,11 +290,26 @@ pub const Options = struct { |
| 243 | if (self.depfile_path) |depfile_path| { | 290 | if (self.depfile_path) |depfile_path| { |
| 244 | self.allocator.free(depfile_path); | 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 | pub fn dumpVerbose(self: *const Options, writer: anytype) !void { | 298 | pub fn dumpVerbose(self: *const Options, writer: anytype) !void { |
| 249 | try writer.print("Input filename: {s}\n", .{self.input_filename}); | 299 | const input_source_name = switch (self.input_source) { |
| 250 | try writer.print("Output filename: {s}\n", .{self.output_filename}); | 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 | if (self.extra_include_paths.items.len > 0) { | 313 | if (self.extra_include_paths.items.len > 0) { |
| 252 | try writer.writeAll(" Extra include paths:\n"); | 314 | try writer.writeAll(" Extra include paths:\n"); |
| 253 | for (self.extra_include_paths.items) |extra_include_path| { | 315 | for (self.extra_include_paths.items) |extra_include_path| { |
| ... | @@ -331,6 +393,7 @@ pub const Arg = struct { | ... | @@ -331,6 +393,7 @@ pub const Arg = struct { |
| 331 | } | 393 | } |
| 332 | 394 | ||
| 333 | pub fn optionWithoutPrefix(self: Arg, option_len: usize) []const u8 { | 395 | pub fn optionWithoutPrefix(self: Arg, option_len: usize) []const u8 { |
| 396 | if (option_len == 0) return self.name(); | ||
| 334 | return self.name()[0..option_len]; | 397 | return self.name()[0..option_len]; |
| 335 | } | 398 | } |
| 336 | 399 | ||
| ... | @@ -380,6 +443,8 @@ pub const Arg = struct { | ... | @@ -380,6 +443,8 @@ pub const Arg = struct { |
| 380 | 443 | ||
| 381 | pub const Value = struct { | 444 | pub const Value = struct { |
| 382 | slice: []const u8, | 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 | index_increment: u2 = 1, | 448 | index_increment: u2 = 1, |
| 384 | 449 | ||
| 385 | pub fn argSpan(self: Value, arg: Arg) Diagnostics.ErrorDetails.ArgSpan { | 450 | pub fn argSpan(self: Value, arg: Arg) Diagnostics.ErrorDetails.ArgSpan { |
| ... | @@ -414,6 +479,7 @@ pub const Arg = struct { | ... | @@ -414,6 +479,7 @@ pub const Arg = struct { |
| 414 | 479 | ||
| 415 | pub const Context = struct { | 480 | pub const Context = struct { |
| 416 | index: usize, | 481 | index: usize, |
| 482 | option_len: usize, | ||
| 417 | arg: Arg, | 483 | arg: Arg, |
| 418 | value: Value, | 484 | value: Value, |
| 419 | }; | 485 | }; |
| ... | @@ -428,7 +494,18 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn | ... | @@ -428,7 +494,18 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn |
| 428 | errdefer options.deinit(); | 494 | errdefer options.deinit(); |
| 429 | 495 | ||
| 430 | var output_filename: ?[]const u8 = null; | 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 | var arg_i: usize = 0; | 510 | var arg_i: usize = 0; |
| 434 | next_arg: while (arg_i < args.len) { | 511 | next_arg: while (arg_i < args.len) { |
| ... | @@ -470,6 +547,25 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn | ... | @@ -470,6 +547,25 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn |
| 470 | if (std.ascii.startsWithIgnoreCase(arg_name, ":no-preprocess")) { | 547 | if (std.ascii.startsWithIgnoreCase(arg_name, ":no-preprocess")) { |
| 471 | options.preprocess = .no; | 548 | options.preprocess = .no; |
| 472 | arg.name_offset += ":no-preprocess".len; | 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 | } else if (std.ascii.startsWithIgnoreCase(arg_name, ":auto-includes")) { | 569 | } else if (std.ascii.startsWithIgnoreCase(arg_name, ":auto-includes")) { |
| 474 | const value = arg.value(":auto-includes".len, arg_i, args) catch { | 570 | const value = arg.value(":auto-includes".len, arg_i, args) catch { |
| 475 | var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; | 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,6 +584,25 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn |
| 488 | }; | 584 | }; |
| 489 | arg_i += value.index_increment; | 585 | arg_i += value.index_increment; |
| 490 | continue :next_arg; | 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 | } else if (std.ascii.startsWithIgnoreCase(arg_name, ":depfile-fmt")) { | 606 | } else if (std.ascii.startsWithIgnoreCase(arg_name, ":depfile-fmt")) { |
| 492 | const value = arg.value(":depfile-fmt".len, arg_i, args) catch { | 607 | const value = arg.value(":depfile-fmt".len, arg_i, args) catch { |
| 493 | var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; | 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,6 +637,31 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn |
| 522 | const path = try allocator.dupe(u8, value.slice); | 637 | const path = try allocator.dupe(u8, value.slice); |
| 523 | errdefer allocator.free(path); | 638 | errdefer allocator.free(path); |
| 524 | options.depfile_path = path; | 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 | arg_i += value.index_increment; | 665 | arg_i += value.index_increment; |
| 526 | continue :next_arg; | 666 | continue :next_arg; |
| 527 | } else if (std.ascii.startsWithIgnoreCase(arg_name, "nologo")) { | 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,7 +760,7 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn |
| 620 | arg_i += 1; | 760 | arg_i += 1; |
| 621 | break :next_arg; | 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 | output_filename = value.slice; | 764 | output_filename = value.slice; |
| 625 | arg_i += value.index_increment; | 765 | arg_i += value.index_increment; |
| 626 | continue :next_arg; | 766 | continue :next_arg; |
| ... | @@ -812,6 +952,7 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn | ... | @@ -812,6 +952,7 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn |
| 812 | arg.name_offset += 1; | 952 | arg.name_offset += 1; |
| 813 | } else if (std.ascii.startsWithIgnoreCase(arg_name, "p")) { | 953 | } else if (std.ascii.startsWithIgnoreCase(arg_name, "p")) { |
| 814 | options.preprocess = .only; | 954 | options.preprocess = .only; |
| 955 | preprocess_only_context = .{ .index = arg_i, .option_len = "p".len, .arg = arg, .value = undefined }; | ||
| 815 | arg.name_offset += 1; | 956 | arg.name_offset += 1; |
| 816 | } else if (std.ascii.startsWithIgnoreCase(arg_name, "i")) { | 957 | } else if (std.ascii.startsWithIgnoreCase(arg_name, "i")) { |
| 817 | const value = arg.value(1, arg_i, args) catch { | 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,10 +1061,10 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn |
| 920 | 1061 | ||
| 921 | if (args.len > 0) { | 1062 | if (args.len > 0) { |
| 922 | const last_arg = args[args.len - 1]; | 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 | var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = true, .arg_index = arg_i - 1 }; | 1065 | var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = true, .arg_index = arg_i - 1 }; |
| 925 | var note_writer = note_details.msg.writer(allocator); | 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 | try diagnostics.append(note_details); | 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,7 +1073,28 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn |
| 932 | // things after this rely on the value of the input filename. | 1073 | // things after this rely on the value of the input filename. |
| 933 | return error.ParseError; | 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 | if (positionals.len > 1) { | 1099 | if (positionals.len > 1) { |
| 938 | if (output_filename != null) { | 1100 | if (output_filename != null) { |
| ... | @@ -942,53 +1104,233 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn | ... | @@ -942,53 +1104,233 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn |
| 942 | try diagnostics.append(err_details); | 1104 | try diagnostics.append(err_details); |
| 943 | var note_details = Diagnostics.ErrorDetails{ | 1105 | var note_details = Diagnostics.ErrorDetails{ |
| 944 | .type = .note, | 1106 | .type = .note, |
| 945 | .arg_index = output_filename_context.value.index(output_filename_context.index), | 1107 | .arg_index = output_filename_context.arg.index, |
| 946 | .arg_span = output_filename_context.value.argSpan(output_filename_context.arg), | 1108 | .arg_span = output_filename_context.arg.value.argSpan(output_filename_context.arg.arg), |
| 947 | }; | 1109 | }; |
| 948 | var note_writer = note_details.msg.writer(allocator); | 1110 | var note_writer = note_details.msg.writer(allocator); |
| 949 | try note_writer.writeAll("output filename previously specified here"); | 1111 | try note_writer.writeAll("output filename previously specified here"); |
| 950 | try diagnostics.append(note_details); | 1112 | try diagnostics.append(note_details); |
| 951 | } else { | 1113 | } else { |
| 952 | output_filename = positionals[1]; | 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 | if (output_filename == null) { | 1129 | if (output_filename == null) { |
| 956 | var buf = std.ArrayList(u8).init(allocator); | 1130 | if (output_format == null) { |
| 957 | errdefer buf.deinit(); | 1131 | output_format_source = .inferred_from_input_filename; |
| 958 | 1132 | const input_ext = std.fs.path.extension(options.input_source.filename); | |
| 959 | if (std.fs.path.dirname(options.input_filename)) |dirname| { | 1133 | if (std.ascii.eqlIgnoreCase(input_ext, ".res")) { |
| 960 | var end_pos = dirname.len; | 1134 | output_format = .coff; |
| 961 | // We want to ensure that we write a path separator at the end, so if the dirname | 1135 | } else if (options.preprocess == .only and (input_format.? == .rc or std.ascii.eqlIgnoreCase(input_ext, ".rc"))) { |
| 962 | // doesn't end with a path sep then include the char after the dirname | 1136 | output_format = .rcpp; |
| 963 | // which must be a path sep. | 1137 | output_format_source = .inferred_from_preprocess_only; |
| 964 | if (!std.fs.path.isSep(dirname[dirname.len - 1])) end_pos += 1; | 1138 | } else { |
| 965 | try buf.appendSlice(options.input_filename[0..end_pos]); | 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)); | 1145 | options.output_source = .{ .filename = try filepathWithExtension(allocator, options.input_source.filename, output_format.?.extension()) }; |
| 968 | if (options.preprocess == .only) { | 1146 | } else { |
| 969 | try buf.appendSlice(".rcpp"); | 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 | } else { | 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(); | 1166 | options.input_format = input_format.?; |
| 975 | } else { | 1167 | options.output_format = output_format.?; |
| 976 | options.output_filename = try allocator.dupe(u8, output_filename.?); | 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 | if (diagnostics.hasError()) { | 1279 | if (diagnostics.hasError()) { |
| 980 | return error.ParseError; | 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 | return options; | 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 | pub fn isSupportedInputExtension(ext: []const u8) bool { | 1307 | pub fn isSupportedInputExtension(ext: []const u8) bool { |
| 987 | if (std.ascii.eqlIgnoreCase(ext, ".rc")) return true; | 1308 | if (std.ascii.eqlIgnoreCase(ext, ".rc")) return true; |
| 1309 | if (std.ascii.eqlIgnoreCase(ext, ".res")) return true; | ||
| 988 | if (std.ascii.eqlIgnoreCase(ext, ".rcpp")) return true; | 1310 | if (std.ascii.eqlIgnoreCase(ext, ".rcpp")) return true; |
| 989 | return false; | 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 | /// Returns true if the str is a valid C identifier for use in a #define/#undef macro | 1334 | /// Returns true if the str is a valid C identifier for use in a #define/#undef macro |
| 993 | pub fn isValidIdentifier(str: []const u8) bool { | 1335 | pub fn isValidIdentifier(str: []const u8) bool { |
| 994 | for (str, 0..) |c, i| switch (c) { | 1336 | for (str, 0..) |c, i| switch (c) { |
| ... | @@ -1278,17 +1620,6 @@ test "parse errors: basic" { | ... | @@ -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 | test "inferred absolute filepaths" { | 1625 | test "inferred absolute filepaths" { |
| ... | @@ -1349,8 +1680,8 @@ test "parse: options" { | ... | @@ -1349,8 +1680,8 @@ test "parse: options" { |
| 1349 | defer options.deinit(); | 1680 | defer options.deinit(); |
| 1350 | 1681 | ||
| 1351 | try std.testing.expectEqual(true, options.verbose); | 1682 | try std.testing.expectEqual(true, options.verbose); |
| 1352 | try std.testing.expectEqualStrings("foo.rc", options.input_filename); | 1683 | try std.testing.expectEqualStrings("foo.rc", options.input_source.filename); |
| 1353 | try std.testing.expectEqualStrings("foo.res", options.output_filename); | 1684 | try std.testing.expectEqualStrings("foo.res", options.output_source.filename); |
| 1354 | } | 1685 | } |
| 1355 | { | 1686 | { |
| 1356 | var options = try testParse(&.{ "/vx", "foo.rc" }); | 1687 | var options = try testParse(&.{ "/vx", "foo.rc" }); |
| ... | @@ -1358,8 +1689,8 @@ test "parse: options" { | ... | @@ -1358,8 +1689,8 @@ test "parse: options" { |
| 1358 | 1689 | ||
| 1359 | try std.testing.expectEqual(true, options.verbose); | 1690 | try std.testing.expectEqual(true, options.verbose); |
| 1360 | try std.testing.expectEqual(true, options.ignore_include_env_var); | 1691 | try std.testing.expectEqual(true, options.ignore_include_env_var); |
| 1361 | try std.testing.expectEqualStrings("foo.rc", options.input_filename); | 1692 | try std.testing.expectEqualStrings("foo.rc", options.input_source.filename); |
| 1362 | try std.testing.expectEqualStrings("foo.res", options.output_filename); | 1693 | try std.testing.expectEqualStrings("foo.res", options.output_source.filename); |
| 1363 | } | 1694 | } |
| 1364 | { | 1695 | { |
| 1365 | var options = try testParse(&.{ "/xv", "foo.rc" }); | 1696 | var options = try testParse(&.{ "/xv", "foo.rc" }); |
| ... | @@ -1367,8 +1698,8 @@ test "parse: options" { | ... | @@ -1367,8 +1698,8 @@ test "parse: options" { |
| 1367 | 1698 | ||
| 1368 | try std.testing.expectEqual(true, options.verbose); | 1699 | try std.testing.expectEqual(true, options.verbose); |
| 1369 | try std.testing.expectEqual(true, options.ignore_include_env_var); | 1700 | try std.testing.expectEqual(true, options.ignore_include_env_var); |
| 1370 | try std.testing.expectEqualStrings("foo.rc", options.input_filename); | 1701 | try std.testing.expectEqualStrings("foo.rc", options.input_source.filename); |
| 1371 | try std.testing.expectEqualStrings("foo.res", options.output_filename); | 1702 | try std.testing.expectEqualStrings("foo.res", options.output_source.filename); |
| 1372 | } | 1703 | } |
| 1373 | { | 1704 | { |
| 1374 | var options = try testParse(&.{ "/xvFObar.res", "foo.rc" }); | 1705 | var options = try testParse(&.{ "/xvFObar.res", "foo.rc" }); |
| ... | @@ -1376,8 +1707,8 @@ test "parse: options" { | ... | @@ -1376,8 +1707,8 @@ test "parse: options" { |
| 1376 | 1707 | ||
| 1377 | try std.testing.expectEqual(true, options.verbose); | 1708 | try std.testing.expectEqual(true, options.verbose); |
| 1378 | try std.testing.expectEqual(true, options.ignore_include_env_var); | 1709 | try std.testing.expectEqual(true, options.ignore_include_env_var); |
| 1379 | try std.testing.expectEqualStrings("foo.rc", options.input_filename); | 1710 | try std.testing.expectEqualStrings("foo.rc", options.input_source.filename); |
| 1380 | try std.testing.expectEqualStrings("bar.res", options.output_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,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 | test "maybeAppendRC" { | 2052 | test "maybeAppendRC" { |
| 1545 | var tmp = std.testing.tmpDir(.{}); | 2053 | var tmp = std.testing.tmpDir(.{}); |
| 1546 | defer tmp.cleanup(); | 2054 | defer tmp.cleanup(); |
| 1547 | 2055 | ||
| 1548 | var options = try testParse(&.{"foo"}); | 2056 | var options = try testParse(&.{"foo"}); |
| 1549 | defer options.deinit(); | 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 | // Create the file so that it's found. In this scenario, .rc should not get | 2060 | // Create the file so that it's found. In this scenario, .rc should not get |
| 1553 | // appended. | 2061 | // appended. |
| 1554 | var file = try tmp.dir.createFile("foo", .{}); | 2062 | var file = try tmp.dir.createFile("foo", .{}); |
| 1555 | file.close(); | 2063 | file.close(); |
| 1556 | try options.maybeAppendRC(tmp.dir); | 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 | 2067 | // Now delete the file and try again. But this time change the input format |
| 1560 | // and the input filename does not have an extension, .rc should get appended. | 2068 | // to non-rc. |
| 1561 | try tmp.dir.deleteFile("foo"); | 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 | try options.maybeAppendRC(tmp.dir); | 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 +7,10 @@ const Diagnostics = @import("errors.zig").Diagnostics; |
| 7 | const cli = @import("cli.zig"); | 7 | const cli = @import("cli.zig"); |
| 8 | const preprocess = @import("preprocess.zig"); | 8 | const preprocess = @import("preprocess.zig"); |
| 9 | const renderErrorMessage = @import("utils.zig").renderErrorMessage; | 9 | const renderErrorMessage = @import("utils.zig").renderErrorMessage; |
| 10 | const openFileNotDir = @import("utils.zig").openFileNotDir; | ||
| 11 | const cvtres = @import("cvtres.zig"); | ||
| 10 | const hasDisjointCodePage = @import("disjoint_code_page.zig").hasDisjointCodePage; | 12 | const hasDisjointCodePage = @import("disjoint_code_page.zig").hasDisjointCodePage; |
| 13 | const fmtResourceType = @import("res.zig").NameOrOrdinal.fmtResourceType; | ||
| 11 | const aro = @import("aro"); | 14 | const aro = @import("aro"); |
| 12 | 15 | ||
| 13 | pub fn main() !void { | 16 | pub fn main() !void { |
| ... | @@ -135,7 +138,10 @@ pub fn main() !void { | ... | @@ -135,7 +138,10 @@ pub fn main() !void { |
| 135 | 138 | ||
| 136 | try argv.append("arocc"); // dummy command name | 139 | try argv.append("arocc"); // dummy command name |
| 137 | try preprocess.appendAroArgs(aro_arena, &argv, options, include_paths); | 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 | if (options.verbose) { | 146 | if (options.verbose) { |
| 141 | try stdout_writer.writeAll("Preprocessor: arocc (built-in)\n"); | 147 | try stdout_writer.writeAll("Preprocessor: arocc (built-in)\n"); |
| ... | @@ -164,120 +170,332 @@ pub fn main() !void { | ... | @@ -164,120 +170,332 @@ pub fn main() !void { |
| 164 | 170 | ||
| 165 | break :full_input try preprocessed_buf.toOwnedSlice(); | 171 | break :full_input try preprocessed_buf.toOwnedSlice(); |
| 166 | } else { | 172 | } else { |
| 167 | break :full_input std.fs.cwd().readFileAlloc(allocator, options.input_filename, std.math.maxInt(usize)) catch |err| { | 173 | switch (options.input_source) { |
| 168 | try error_handler.emitMessage(allocator, .err, "unable to read input file path '{s}': {s}", .{ options.input_filename, @errorName(err) }); | 174 | .stdio => |file| { |
| 169 | std.process.exit(1); | 175 | break :full_input file.readToEndAlloc(allocator, std.math.maxInt(usize)) catch |err| { |
| 170 | }; | 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 | defer allocator.free(full_input); | 189 | defer allocator.free(full_input); |
| 174 | 190 | ||
| 175 | if (options.preprocess == .only) { | 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 | return; | 200 | return; |
| 178 | } | 201 | } |
| 179 | 202 | ||
| 180 | // Note: We still want to run this when no-preprocess is set because: | 203 | var resources = resources: { |
| 181 | // 1. We want to print accurate line numbers after removing multiline comments | 204 | const need_intermediate_res = options.output_format == .coff and options.input_format != .res; |
| 182 | // 2. We want to be able to handle an already-preprocessed input with #line commands in it | 205 | var res_stream = if (need_intermediate_res) |
| 183 | var mapping_results = parseAndRemoveLineCommands(allocator, full_input, full_input, .{ .initial_filename = options.input_filename }) catch |err| switch (err) { | 206 | IoStream{ |
| 184 | error.InvalidLineCommand => { | 207 | .name = "<in-memory intermediate res>", |
| 185 | // TODO: Maybe output the invalid line command | 208 | .intermediate = true, |
| 186 | try error_handler.emitMessage(allocator, .err, "invalid line command in the preprocessed source", .{}); | 209 | .source = .{ .memory = .empty }, |
| 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", .{}); | ||
| 191 | } | 210 | } |
| 192 | std.process.exit(1); | 211 | else if (options.input_format == .res) |
| 193 | }, | 212 | IoStream.fromIoSource(options.input_source, .input) catch |err| { |
| 194 | error.LineNumberOverflow => { | 213 | try error_handler.emitMessage(allocator, .err, "unable to read res file path '{s}': {s}", .{ options.input_source.filename, @errorName(err) }); |
| 195 | // TODO: Better error message | 214 | std.process.exit(1); |
| 196 | try error_handler.emitMessage(allocator, .err, "line number count exceeded maximum of {}", .{std.math.maxInt(usize)}); | 215 | } |
| 197 | std.process.exit(1); | 216 | else |
| 198 | }, | 217 | IoStream.fromIoSource(options.output_source, .output) catch |err| { |
| 199 | error.OutOfMemory => |e| return e, | 218 | try error_handler.emitMessage(allocator, .err, "unable to create output file '{s}': {s}", .{ options.output_source.filename, @errorName(err) }); |
| 200 | }; | 219 | std.process.exit(1); |
| 201 | defer mapping_results.mappings.deinit(allocator); | 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; | 285 | try output_buffered_stream.flush(); |
| 204 | const has_disjoint_code_page = hasDisjointCodePage(mapping_results.result, &mapping_results.mappings, default_code_page); | ||
| 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| { | 292 | // write the depfile |
| 209 | try error_handler.emitMessage(allocator, .err, "unable to create output file '{s}': {s}", .{ options.output_filename, @errorName(err) }); | 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 | std.process.exit(1); | 342 | std.process.exit(1); |
| 211 | }; | 343 | }; |
| 212 | var output_file_closed = false; | 344 | defer coff_stream.deinit(allocator); |
| 213 | defer if (!output_file_closed) output_file.close(); | 345 | |
| 214 | 346 | var coff_output_buffered_stream = std.io.bufferedWriter(coff_stream.source.writer(allocator)); | |
| 215 | var diagnostics = Diagnostics.init(allocator); | 347 | |
| 216 | defer diagnostics.deinit(); | 348 | var cvtres_diagnostics: cvtres.Diagnostics = .{ .none = {} }; |
| 217 | 349 | cvtres.writeCoff(allocator, coff_output_buffered_stream.writer(), resources.list.items, options.coff_options, &cvtres_diagnostics) catch |err| { | |
| 218 | var output_buffered_stream = std.io.bufferedWriter(output_file.writer()); | 350 | switch (err) { |
| 219 | 351 | error.DuplicateResource => { | |
| 220 | compile(allocator, final_input, output_buffered_stream.writer(), .{ | 352 | const duplicate_resource = resources.list.items[cvtres_diagnostics.duplicate_resource]; |
| 221 | .cwd = std.fs.cwd(), | 353 | try error_handler.emitMessage(allocator, .err, "duplicate resource [id: {}, type: {}, language: {}]", .{ |
| 222 | .diagnostics = &diagnostics, | 354 | duplicate_resource.name_value, |
| 223 | .source_mappings = &mapping_results.mappings, | 355 | fmtResourceType(duplicate_resource.type_value), |
| 224 | .dependencies_list = maybe_dependencies_list, | 356 | duplicate_resource.language, |
| 225 | .ignore_include_env_var = options.ignore_include_env_var, | 357 | }); |
| 226 | .extra_include_paths = options.extra_include_paths.items, | 358 | }, |
| 227 | .system_include_paths = include_paths, | 359 | error.ResourceDataTooLong => { |
| 228 | .default_language_id = options.default_language_id, | 360 | const overflow_resource = resources.list.items[cvtres_diagnostics.duplicate_resource]; |
| 229 | .default_code_page = default_code_page, | 361 | try error_handler.emitMessage(allocator, .err, "resource has a data length that is too large to be written into a coff section", .{}); |
| 230 | .disjoint_code_page = has_disjoint_code_page, | 362 | try error_handler.emitMessage(allocator, .note, "the resource with the invalid size is [id: {}, type: {}, language: {}]", .{ |
| 231 | .verbose = options.verbose, | 363 | overflow_resource.name_value, |
| 232 | .null_terminate_string_table_strings = options.null_terminate_string_table_strings, | 364 | fmtResourceType(overflow_resource.type_value), |
| 233 | .max_string_literal_codepoints = options.max_string_literal_codepoints, | 365 | overflow_resource.language, |
| 234 | .silent_duplicate_control_ids = options.silent_duplicate_control_ids, | 366 | }); |
| 235 | .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page, | 367 | }, |
| 236 | }) catch |err| switch (err) { | 368 | error.TotalResourceDataTooLong => { |
| 237 | error.ParseError, error.CompileError => { | 369 | const overflow_resource = resources.list.items[cvtres_diagnostics.duplicate_resource]; |
| 238 | try error_handler.emitDiagnostics(allocator, std.fs.cwd(), final_input, &diagnostics, mapping_results.mappings); | 370 | try error_handler.emitMessage(allocator, .err, "total resource data exceeds the maximum of the coff 'size of raw data' field", .{}); |
| 239 | // Delete the output file on error | 371 | try error_handler.emitMessage(allocator, .note, "size overflow occurred when attempting to write this resource: [id: {}, type: {}, language: {}]", .{ |
| 240 | output_file.close(); | 372 | overflow_resource.name_value, |
| 241 | output_file_closed = true; | 373 | fmtResourceType(overflow_resource.type_value), |
| 242 | // Failing to delete is not really a big deal, so swallow any errors | 374 | overflow_resource.language, |
| 243 | std.fs.cwd().deleteFile(options.output_filename) catch {}; | 375 | }); |
| 244 | std.process.exit(1); | 376 | }, |
| 245 | }, | 377 | else => { |
| 246 | else => |e| return e, | 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 | 389 | const IoStream = struct { |
| 252 | if (!zig_integration) { | 390 | name: []const u8, |
| 253 | diagnostics.renderToStdErr(std.fs.cwd(), final_input, stderr_config, mapping_results.mappings); | 391 | intermediate: bool, |
| 254 | } | 392 | source: Source, |
| 255 | 393 | ||
| 256 | // write the depfile | 394 | pub const IoDirection = enum { input, output }; |
| 257 | if (options.depfile_path) |depfile_path| { | 395 | |
| 258 | var depfile = std.fs.cwd().createFile(depfile_path, .{}) catch |err| { | 396 | pub fn fromIoSource(source: cli.Options.IoSource, io: IoDirection) !IoStream { |
| 259 | try error_handler.emitMessage(allocator, .err, "unable to create depfile '{s}': {s}", .{ depfile_path, @errorName(err) }); | 397 | return .{ |
| 260 | std.process.exit(1); | 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(); | 408 | } |
| 263 | 409 | ||
| 264 | const depfile_writer = depfile.writer(); | 410 | pub fn deinit(self: *IoStream, allocator: std.mem.Allocator) void { |
| 265 | var depfile_buffered_writer = std.io.bufferedWriter(depfile_writer); | 411 | self.source.deinit(allocator); |
| 266 | switch (options.depfile_fmt) { | 412 | } |
| 267 | .json => { | 413 | |
| 268 | var write_stream = std.json.writeStream(depfile_buffered_writer.writer(), .{ .whitespace = .indent_2 }); | 414 | pub fn cleanupAfterError(self: *IoStream) void { |
| 269 | defer write_stream.deinit(); | 415 | switch (self.source) { |
| 270 | 416 | .file => |file| { | |
| 271 | try write_stream.beginArray(); | 417 | // Delete the output file on error |
| 272 | for (dependencies_list.items) |dep_path| { | 418 | file.close(); |
| 273 | try write_stream.write(dep_path); | 419 | // Failing to delete is not really a big deal, so swallow any errors |
| 274 | } | 420 | std.fs.cwd().deleteFile(self.name) catch {}; |
| 275 | try write_stream.endArray(); | ||
| 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 | fn getIncludePaths(arena: std.mem.Allocator, auto_includes_option: cli.Options.AutoIncludes, zig_lib_dir: []const u8) ![]const []const u8 { | 500 | fn getIncludePaths(arena: std.mem.Allocator, auto_includes_option: cli.Options.AutoIncludes, zig_lib_dir: []const u8) ![]const []const u8 { |
| 283 | var includes = auto_includes_option; | 501 | var includes = auto_includes_option; |