authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2023-10-05 03:30:31-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-10-12 16:19:34-07:00
log2769215b9037bb1f407e07887d4d21957ce1a9e0
tree9a89a0975e83ca7be8c407e24ad52cc88f370f2b
parent375bb5f4a1ebfc1d002e252d97077da7e1db7e4d

Add `zig rc` subcommand, a drop-in replacement for rc.exe

Uses resinator under-the-hood (see https://github.com/ziglang/zig/pull/17069) Closes #9564

6 files changed, 407 insertions(+), 55 deletions(-)

src/Compilation.zig+8-53
......@@ -4607,63 +4607,18 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32
46074607
46084608 var argv = std.ArrayList([]const u8).init(comp.gpa);
46094609 defer argv.deinit();
4610 var temp_strings = std.ArrayList([]const u8).init(comp.gpa);
4611 defer {
4612 for (temp_strings.items) |temp_string| {
4613 comp.gpa.free(temp_string);
4614 }
4615 temp_strings.deinit();
4616 }
46174610
46184611 // TODO: support options.preprocess == .no and .only
46194612 // alternatively, error if those options are used
4620 try argv.appendSlice(&[_][]const u8{
4621 self_exe_path,
4622 "clang",
4623 "-E", // preprocessor only
4624 "--comments",
4625 "-fuse-line-directives", // #line <num> instead of # <num>
4626 "-xc", // output c
4627 "-Werror=null-character", // error on null characters instead of converting them to spaces
4628 "-fms-compatibility", // Allow things like "header.h" to be resolved relative to the 'root' .rc file, among other things
4629 "-DRC_INVOKED", // https://learn.microsoft.com/en-us/windows/win32/menurc/predefined-macros
4613 try argv.appendSlice(&[_][]const u8{ self_exe_path, "clang" });
4614
4615 try resinator.preprocess.appendClangArgs(arena, &argv, options, .{
4616 .clang_target = null, // handled by addCCArgs
4617 .system_include_paths = &.{}, // handled by addCCArgs
4618 .needs_gnu_workaround = comp.getTarget().isGnu(),
4619 .nostdinc = false, // handled by addCCArgs
46304620 });
4631 // Using -fms-compatibility and targeting the gnu abi interact in a strange way:
4632 // - Targeting the GNU abi stops _MSC_VER from being defined
4633 // - Passing -fms-compatibility stops __GNUC__ from being defined
4634 // Neither being defined is a problem for things like things like MinGW's
4635 // vadefs.h, which will fail during preprocessing if neither are defined.
4636 // So, when targeting the GNU abi, we need to force __GNUC__ to be defined.
4637 //
4638 // TODO: This is a workaround that should be removed if possible.
4639 if (comp.getTarget().isGnu()) {
4640 // This is the same default gnuc version that Clang uses:
4641 // https://github.com/llvm/llvm-project/blob/4b5366c9512aa273a5272af1d833961e1ed156e7/clang/lib/Driver/ToolChains/Clang.cpp#L6738
4642 try argv.append("-fgnuc-version=4.2.1");
4643 }
4644 for (options.extra_include_paths.items) |extra_include_path| {
4645 try argv.append("--include-directory");
4646 try argv.append(extra_include_path);
4647 }
4648 var symbol_it = options.symbols.iterator();
4649 while (symbol_it.next()) |entry| {
4650 switch (entry.value_ptr.*) {
4651 .define => |value| {
4652 try argv.append("-D");
4653 const define_arg = arg: {
4654 const arg = try std.fmt.allocPrint(comp.gpa, "{s}={s}", .{ entry.key_ptr.*, value });
4655 errdefer comp.gpa.free(arg);
4656 try temp_strings.append(arg);
4657 break :arg arg;
4658 };
4659 try argv.append(define_arg);
4660 },
4661 .undefine => {
4662 try argv.append("-U");
4663 try argv.append(entry.key_ptr.*);
4664 },
4665 }
4666 }
4621
46674622 try argv.append(win32_resource.src.src_path);
46684623 try argv.appendSlice(&[_][]const u8{
46694624 "-o",
src/main.zig+267
......@@ -104,6 +104,7 @@ const normal_usage =
104104 \\ lib Use Zig as a drop-in lib.exe
105105 \\ ranlib Use Zig as a drop-in ranlib
106106 \\ objcopy Use Zig as a drop-in objcopy
107 \\ rc Use Zig as a drop-in rc.exe
107108 \\
108109 \\ env Print lib path, std path, cache directory, and version
109110 \\ help Print this help and exit
......@@ -300,6 +301,8 @@ pub fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi
300301 return buildOutputType(gpa, arena, args, .cpp);
301302 } else if (mem.eql(u8, cmd, "translate-c")) {
302303 return buildOutputType(gpa, arena, args, .translate_c);
304 } else if (mem.eql(u8, cmd, "rc")) {
305 return cmdRc(gpa, arena, args[1..]);
303306 } else if (mem.eql(u8, cmd, "fmt")) {
304307 return cmdFmt(gpa, arena, cmd_args);
305308 } else if (mem.eql(u8, cmd, "objcopy")) {
......@@ -4372,6 +4375,270 @@ fn cmdTranslateC(comp: *Compilation, arena: Allocator, fancy_output: ?*Compilati
43724375 }
43734376}
43744377
4378fn cmdRc(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
4379 const resinator = @import("resinator.zig");
4380
4381 const stderr = std.io.getStdErr();
4382 const stderr_config = std.io.tty.detectConfig(stderr);
4383
4384 var options = options: {
4385 var cli_diagnostics = resinator.cli.Diagnostics.init(gpa);
4386 defer cli_diagnostics.deinit();
4387 var options = resinator.cli.parse(gpa, args, &cli_diagnostics) catch |err| switch (err) {
4388 error.ParseError => {
4389 cli_diagnostics.renderToStdErr(args, stderr_config);
4390 process.exit(1);
4391 },
4392 else => |e| return e,
4393 };
4394 try options.maybeAppendRC(std.fs.cwd());
4395
4396 // print any warnings/notes
4397 cli_diagnostics.renderToStdErr(args, stderr_config);
4398 // If there was something printed, then add an extra newline separator
4399 // so that there is a clear separation between the cli diagnostics and whatever
4400 // gets printed after
4401 if (cli_diagnostics.errors.items.len > 0) {
4402 std.debug.print("\n", .{});
4403 }
4404 break :options options;
4405 };
4406 defer options.deinit();
4407
4408 if (options.print_help_and_exit) {
4409 try resinator.cli.writeUsage(stderr.writer(), "zig rc");
4410 return;
4411 }
4412
4413 const stdout_writer = std.io.getStdOut().writer();
4414 if (options.verbose) {
4415 try options.dumpVerbose(stdout_writer);
4416 try stdout_writer.writeByte('\n');
4417 }
4418
4419 var full_input = full_input: {
4420 if (options.preprocess != .no) {
4421 if (!build_options.have_llvm) {
4422 fatal("clang not available: compiler built without LLVM extensions", .{});
4423 }
4424
4425 var argv = std.ArrayList([]const u8).init(gpa);
4426 defer argv.deinit();
4427
4428 const self_exe_path = try introspect.findZigExePath(arena);
4429 var zig_lib_directory = introspect.findZigLibDirFromSelfExe(arena, self_exe_path) catch |err| {
4430 try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to find zig installation directory: {s}", .{@errorName(err)});
4431 process.exit(1);
4432 };
4433 defer zig_lib_directory.handle.close();
4434
4435 const include_args = detectRcIncludeDirs(arena, zig_lib_directory.path.?, options.auto_includes) catch |err| {
4436 try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to detect system include directories: {s}", .{@errorName(err)});
4437 process.exit(1);
4438 };
4439
4440 try argv.appendSlice(&[_][]const u8{ self_exe_path, "clang" });
4441
4442 const clang_target = clang_target: {
4443 if (include_args.target_abi) |abi| {
4444 break :clang_target try std.fmt.allocPrint(arena, "x86_64-unknown-windows-{s}", .{abi});
4445 }
4446 break :clang_target "x86_64-unknown-windows";
4447 };
4448 try resinator.preprocess.appendClangArgs(arena, &argv, options, .{
4449 .clang_target = clang_target,
4450 .system_include_paths = include_args.include_paths,
4451 .needs_gnu_workaround = if (include_args.target_abi) |abi| std.mem.eql(u8, abi, "gnu") else false,
4452 .nostdinc = true,
4453 });
4454
4455 try argv.append(options.input_filename);
4456
4457 if (options.verbose) {
4458 try stdout_writer.writeAll("Preprocessor: zig clang\n");
4459 for (argv.items[0 .. argv.items.len - 1]) |arg| {
4460 try stdout_writer.print("{s} ", .{arg});
4461 }
4462 try stdout_writer.print("{s}\n\n", .{argv.items[argv.items.len - 1]});
4463 }
4464
4465 if (std.process.can_spawn) {
4466 var result = std.ChildProcess.exec(.{
4467 .allocator = gpa,
4468 .argv = argv.items,
4469 .max_output_bytes = std.math.maxInt(u32),
4470 }) catch |err| {
4471 try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to spawn preprocessor child process: {s}", .{@errorName(err)});
4472 process.exit(1);
4473 };
4474 errdefer gpa.free(result.stdout);
4475 defer gpa.free(result.stderr);
4476
4477 switch (result.term) {
4478 .Exited => |code| {
4479 if (code != 0) {
4480 try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "the preprocessor failed with exit code {}:", .{code});
4481 try stderr.writeAll(result.stderr);
4482 try stderr.writeAll("\n");
4483 process.exit(1);
4484 }
4485 },
4486 .Signal, .Stopped, .Unknown => {
4487 try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "the preprocessor terminated unexpectedly ({s}):", .{@tagName(result.term)});
4488 try stderr.writeAll(result.stderr);
4489 try stderr.writeAll("\n");
4490 process.exit(1);
4491 },
4492 }
4493
4494 break :full_input result.stdout;
4495 } else {
4496 // need to use an intermediate file
4497 const rand_int = std.crypto.random.int(u64);
4498 const preprocessed_path = try std.fmt.allocPrint(gpa, "resinator{x}.rcpp", .{rand_int});
4499 defer gpa.free(preprocessed_path);
4500 defer std.fs.cwd().deleteFile(preprocessed_path) catch {};
4501
4502 try argv.appendSlice(&.{ "-o", preprocessed_path });
4503 const exit_code = try clangMain(arena, argv.items);
4504 if (exit_code != 0) {
4505 try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "the preprocessor failed with exit code {}:", .{exit_code});
4506 process.exit(1);
4507 }
4508 break :full_input std.fs.cwd().readFileAlloc(gpa, preprocessed_path, std.math.maxInt(usize)) catch |err| {
4509 try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to read preprocessed file path '{s}': {s}", .{ preprocessed_path, @errorName(err) });
4510 process.exit(1);
4511 };
4512 }
4513 } else {
4514 break :full_input std.fs.cwd().readFileAlloc(gpa, options.input_filename, std.math.maxInt(usize)) catch |err| {
4515 try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to read input file path '{s}': {s}", .{ options.input_filename, @errorName(err) });
4516 process.exit(1);
4517 };
4518 }
4519 };
4520 defer gpa.free(full_input);
4521
4522 if (options.preprocess == .only) {
4523 std.fs.cwd().writeFile(options.output_filename, full_input) catch |err| {
4524 try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to write output file '{s}': {s}", .{ options.output_filename, @errorName(err) });
4525 process.exit(1);
4526 };
4527 return cleanExit();
4528 }
4529
4530 var mapping_results = try resinator.source_mapping.parseAndRemoveLineCommands(gpa, full_input, full_input, .{ .initial_filename = options.input_filename });
4531 defer mapping_results.mappings.deinit(gpa);
4532
4533 var final_input = resinator.comments.removeComments(mapping_results.result, mapping_results.result, &mapping_results.mappings);
4534
4535 var output_file = std.fs.cwd().createFile(options.output_filename, .{}) catch |err| {
4536 try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to create output file '{s}': {s}", .{ options.output_filename, @errorName(err) });
4537 process.exit(1);
4538 };
4539 var output_file_closed = false;
4540 defer if (!output_file_closed) output_file.close();
4541
4542 var diagnostics = resinator.errors.Diagnostics.init(gpa);
4543 defer diagnostics.deinit();
4544
4545 var output_buffered_stream = std.io.bufferedWriter(output_file.writer());
4546
4547 resinator.compile.compile(gpa, final_input, output_buffered_stream.writer(), .{
4548 .cwd = std.fs.cwd(),
4549 .diagnostics = &diagnostics,
4550 .source_mappings = &mapping_results.mappings,
4551 .dependencies_list = null,
4552 .ignore_include_env_var = options.ignore_include_env_var,
4553 .extra_include_paths = options.extra_include_paths.items,
4554 .default_language_id = options.default_language_id,
4555 .default_code_page = options.default_code_page orelse .windows1252,
4556 .verbose = options.verbose,
4557 .null_terminate_string_table_strings = options.null_terminate_string_table_strings,
4558 .max_string_literal_codepoints = options.max_string_literal_codepoints,
4559 .silent_duplicate_control_ids = options.silent_duplicate_control_ids,
4560 .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page,
4561 }) catch |err| switch (err) {
4562 error.ParseError, error.CompileError => {
4563 diagnostics.renderToStdErr(std.fs.cwd(), final_input, stderr_config, mapping_results.mappings);
4564 // Delete the output file on error
4565 output_file.close();
4566 output_file_closed = true;
4567 // Failing to delete is not really a big deal, so swallow any errors
4568 std.fs.cwd().deleteFile(options.output_filename) catch {};
4569 process.exit(1);
4570 },
4571 else => |e| return e,
4572 };
4573
4574 try output_buffered_stream.flush();
4575
4576 // print any warnings/notes
4577 diagnostics.renderToStdErr(std.fs.cwd(), final_input, stderr_config, mapping_results.mappings);
4578
4579 return cleanExit();
4580}
4581
4582const RcIncludeArgs = struct {
4583 include_paths: []const []const u8 = &.{},
4584 target_abi: ?[]const u8 = null,
4585};
4586
4587fn detectRcIncludeDirs(arena: Allocator, zig_lib_dir: []const u8, auto_includes: @import("resinator.zig").cli.Options.AutoIncludes) !RcIncludeArgs {
4588 if (auto_includes == .none) return .{};
4589 var cur_includes = auto_includes;
4590 if (builtin.target.os.tag != .windows) {
4591 switch (cur_includes) {
4592 // MSVC can't be found when the host isn't Windows, so short-circuit.
4593 .msvc => return error.WindowsSdkNotFound,
4594 // Skip straight to gnu since we won't be able to detect MSVC on non-Windows hosts.
4595 .any => cur_includes = .gnu,
4596 .gnu => {},
4597 .none => unreachable,
4598 }
4599 }
4600 while (true) {
4601 switch (cur_includes) {
4602 .any, .msvc => {
4603 const cross_target = std.zig.CrossTarget.parse(.{ .arch_os_abi = "native-windows-msvc" }) catch unreachable;
4604 const target = cross_target.toTarget();
4605 const is_native_abi = cross_target.isNativeAbi();
4606 const detected_libc = Compilation.detectLibCIncludeDirs(arena, zig_lib_dir, target, is_native_abi, true, null) catch |err| {
4607 if (cur_includes == .any) {
4608 // fall back to mingw
4609 cur_includes = .gnu;
4610 continue;
4611 }
4612 return err;
4613 };
4614 if (detected_libc.libc_include_dir_list.len == 0) {
4615 if (cur_includes == .any) {
4616 // fall back to mingw
4617 cur_includes = .gnu;
4618 continue;
4619 }
4620 return error.WindowsSdkNotFound;
4621 }
4622 return .{
4623 .include_paths = detected_libc.libc_include_dir_list,
4624 .target_abi = "msvc",
4625 };
4626 },
4627 .gnu => {
4628 const cross_target = std.zig.CrossTarget.parse(.{ .arch_os_abi = "native-windows-gnu" }) catch unreachable;
4629 const target = cross_target.toTarget();
4630 const is_native_abi = cross_target.isNativeAbi();
4631 const detected_libc = try Compilation.detectLibCIncludeDirs(arena, zig_lib_dir, target, is_native_abi, true, null);
4632 return .{
4633 .include_paths = detected_libc.libc_include_dir_list,
4634 .target_abi = "gnu",
4635 };
4636 },
4637 .none => unreachable,
4638 }
4639 }
4640}
4641
43754642pub const usage_libc =
43764643 \\Usage: zig libc
43774644 \\
src/resinator.zig+1
......@@ -17,6 +17,7 @@ pub const lang = @import("resinator/lang.zig");
1717pub const lex = @import("resinator/lex.zig");
1818pub const literals = @import("resinator/literals.zig");
1919pub const parse = @import("resinator/parse.zig");
20pub const preprocess = @import("resinator/preprocess.zig");
2021pub const rc = @import("resinator/rc.zig");
2122pub const res = @import("resinator/res.zig");
2223pub const source_mapping = @import("resinator/source_mapping.zig");
src/resinator/cli.zig+8-2
......@@ -8,8 +8,8 @@ const lex = @import("lex.zig");
88/// This is what /SL 100 will set the maximum string literal length to
99pub const max_string_literal_length_100_percent = 8192;
1010
11pub const usage_string =
12 \\Usage: resinator [options] [--] <INPUT> [<OUTPUT>]
11pub const usage_string_after_command_name =
12 \\ [options] [--] <INPUT> [<OUTPUT>]
1313 \\
1414 \\The sequence -- can be used to signify when to stop parsing options.
1515 \\This is necessary when the input path begins with a forward slash.
......@@ -57,6 +57,12 @@ pub const usage_string =
5757 \\
5858;
5959
60pub fn writeUsage(writer: anytype, command_name: []const u8) !void {
61 try writer.writeAll("Usage: ");
62 try writer.writeAll(command_name);
63 try writer.writeAll(usage_string_after_command_name);
64}
65
6066pub const Diagnostics = struct {
6167 errors: std.ArrayListUnmanaged(ErrorDetails) = .{},
6268 allocator: Allocator,
src/resinator/preprocess.zig created+94
......@@ -0,0 +1,94 @@
1const std = @import("std");
2const Allocator = std.mem.Allocator;
3const cli = @import("cli.zig");
4
5pub const IncludeArgs = struct {
6 clang_target: ?[]const u8 = null,
7 system_include_paths: []const []const u8,
8 /// Should be set to `true` when -target has the GNU abi
9 /// (either because `clang_target` has `-gnu` or `-target`
10 /// is appended via other means and it has `-gnu`)
11 needs_gnu_workaround: bool = false,
12 nostdinc: bool = false,
13
14 pub const IncludeAbi = enum {
15 msvc,
16 gnu,
17 };
18};
19
20/// `arena` is used for temporary -D argument strings and the INCLUDE environment variable.
21/// The arena should be kept alive at least as long as `argv`.
22pub fn appendClangArgs(arena: Allocator, argv: *std.ArrayList([]const u8), options: cli.Options, include_args: IncludeArgs) !void {
23 try argv.appendSlice(&[_][]const u8{
24 "-E", // preprocessor only
25 "--comments",
26 "-fuse-line-directives", // #line <num> instead of # <num>
27 // TODO: could use --trace-includes to give info about what's included from where
28 "-xc", // output c
29 // TODO: Turn this off, check the warnings, and convert the spaces back to NUL
30 "-Werror=null-character", // error on null characters instead of converting them to spaces
31 // TODO: could remove -Werror=null-character and instead parse warnings looking for 'warning: null character ignored'
32 // since the only real problem is when clang doesn't preserve null characters
33 //"-Werror=invalid-pp-token", // will error on unfinished string literals
34 // TODO: could use -Werror instead
35 "-fms-compatibility", // Allow things like "header.h" to be resolved relative to the 'root' .rc file, among other things
36 // https://learn.microsoft.com/en-us/windows/win32/menurc/predefined-macros
37 "-DRC_INVOKED",
38 });
39 for (options.extra_include_paths.items) |extra_include_path| {
40 try argv.append("-I");
41 try argv.append(extra_include_path);
42 }
43
44 if (include_args.nostdinc) {
45 try argv.append("-nostdinc");
46 }
47 for (include_args.system_include_paths) |include_path| {
48 try argv.append("-isystem");
49 try argv.append(include_path);
50 }
51 if (include_args.clang_target) |target| {
52 try argv.append("-target");
53 try argv.append(target);
54 }
55 // Using -fms-compatibility and targeting the GNU abi interact in a strange way:
56 // - Targeting the GNU abi stops _MSC_VER from being defined
57 // - Passing -fms-compatibility stops __GNUC__ from being defined
58 // Neither being defined is a problem for things like MinGW's vadefs.h,
59 // which will fail during preprocessing if neither are defined.
60 // So, when targeting the GNU abi, we need to force __GNUC__ to be defined.
61 //
62 // TODO: This is a workaround that should be removed if possible.
63 if (include_args.needs_gnu_workaround) {
64 // This is the same default gnuc version that Clang uses:
65 // https://github.com/llvm/llvm-project/blob/4b5366c9512aa273a5272af1d833961e1ed156e7/clang/lib/Driver/ToolChains/Clang.cpp#L6738
66 try argv.append("-fgnuc-version=4.2.1");
67 }
68
69 if (!options.ignore_include_env_var) {
70 const INCLUDE = std.process.getEnvVarOwned(arena, "INCLUDE") catch "";
71
72 // TODO: Should this be platform-specific? How does windres/llvm-rc handle this (if at all)?
73 var it = std.mem.tokenize(u8, INCLUDE, ";");
74 while (it.next()) |include_path| {
75 try argv.append("-isystem");
76 try argv.append(include_path);
77 }
78 }
79
80 var symbol_it = options.symbols.iterator();
81 while (symbol_it.next()) |entry| {
82 switch (entry.value_ptr.*) {
83 .define => |value| {
84 try argv.append("-D");
85 const define_arg = try std.fmt.allocPrint(arena, "{s}={s}", .{ entry.key_ptr.*, value });
86 try argv.append(define_arg);
87 },
88 .undefine => {
89 try argv.append("-U");
90 try argv.append(entry.key_ptr.*);
91 },
92 }
93 }
94}
src/resinator/utils.zig+29
......@@ -81,3 +81,32 @@ pub fn isNonAsciiDigit(c: u21) bool {
8181 else => false,
8282 };
8383}
84
85/// Used for generic colored errors/warnings/notes, more context-specific error messages
86/// are handled elsewhere.
87pub fn renderErrorMessage(writer: anytype, config: std.io.tty.Config, msg_type: enum { err, warning, note }, comptime format: []const u8, args: anytype) !void {
88 switch (msg_type) {
89 .err => {
90 try config.setColor(writer, .bold);
91 try config.setColor(writer, .red);
92 try writer.writeAll("error: ");
93 },
94 .warning => {
95 try config.setColor(writer, .bold);
96 try config.setColor(writer, .yellow);
97 try writer.writeAll("warning: ");
98 },
99 .note => {
100 try config.setColor(writer, .reset);
101 try config.setColor(writer, .cyan);
102 try writer.writeAll("note: ");
103 },
104 }
105 try config.setColor(writer, .reset);
106 if (msg_type == .err) {
107 try config.setColor(writer, .bold);
108 }
109 try writer.print(format, args);
110 try writer.writeByte('\n');
111 try config.setColor(writer, .reset);
112}