Skip to content

Commit 2769215

Browse files
squeek502andrewrk
authored andcommitted
Add zig rc subcommand, a drop-in replacement for rc.exe
Uses resinator under-the-hood (see #17069) Closes #9564
1 parent 375bb5f commit 2769215

File tree

6 files changed

+407
-55
lines changed

6 files changed

+407
-55
lines changed

src/Compilation.zig

Lines changed: 8 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

4378+
fn 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+
4582+
const RcIncludeArgs = struct {
4583+
include_paths: []const []const u8 = &.{},
4584+
target_abi: ?[]const u8 = null,
4585+
};
4586+
4587+
fn 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+
43754642
pub const usage_libc =
43764643
\\Usage: zig libc
43774644
\\

src/resinator.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub const lang = @import("resinator/lang.zig");
1717
pub const lex = @import("resinator/lex.zig");
1818
pub const literals = @import("resinator/literals.zig");
1919
pub const parse = @import("resinator/parse.zig");
20+
pub const preprocess = @import("resinator/preprocess.zig");
2021
pub const rc = @import("resinator/rc.zig");
2122
pub const res = @import("resinator/res.zig");
2223
pub const source_mapping = @import("resinator/source_mapping.zig");

src/resinator/cli.zig

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ const lex = @import("lex.zig");
88
/// This is what /SL 100 will set the maximum string literal length to
99
pub const max_string_literal_length_100_percent = 8192;
1010

11-
pub const usage_string =
12-
\\Usage: resinator [options] [--] <INPUT> [<OUTPUT>]
11+
pub 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

60+
pub 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+
6066
pub const Diagnostics = struct {
6167
errors: std.ArrayListUnmanaged(ErrorDetails) = .{},
6268
allocator: Allocator,

0 commit comments

Comments
 (0)