Skip to content

Commit

Permalink
Add zig rc subcommand, a drop-in replacement for rc.exe
Browse files Browse the repository at this point in the history
Uses resinator under-the-hood (see #17069)

Closes #9564
  • Loading branch information
squeek502 authored and andrewrk committed Oct 12, 2023
1 parent 375bb5f commit 2769215
Showing 6 changed files with 407 additions and 55 deletions.
61 changes: 8 additions & 53 deletions src/Compilation.zig
Original file line number Diff line number Diff line change
@@ -4607,63 +4607,18 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32

var argv = std.ArrayList([]const u8).init(comp.gpa);
defer argv.deinit();
var temp_strings = std.ArrayList([]const u8).init(comp.gpa);
defer {
for (temp_strings.items) |temp_string| {
comp.gpa.free(temp_string);
}
temp_strings.deinit();
}

// TODO: support options.preprocess == .no and .only
// alternatively, error if those options are used
try argv.appendSlice(&[_][]const u8{
self_exe_path,
"clang",
"-E", // preprocessor only
"--comments",
"-fuse-line-directives", // #line <num> instead of # <num>
"-xc", // output c
"-Werror=null-character", // error on null characters instead of converting them to spaces
"-fms-compatibility", // Allow things like "header.h" to be resolved relative to the 'root' .rc file, among other things
"-DRC_INVOKED", // https://learn.microsoft.com/en-us/windows/win32/menurc/predefined-macros
try argv.appendSlice(&[_][]const u8{ self_exe_path, "clang" });

try resinator.preprocess.appendClangArgs(arena, &argv, options, .{
.clang_target = null, // handled by addCCArgs
.system_include_paths = &.{}, // handled by addCCArgs
.needs_gnu_workaround = comp.getTarget().isGnu(),
.nostdinc = false, // handled by addCCArgs
});
// Using -fms-compatibility and targeting the gnu abi interact in a strange way:
// - Targeting the GNU abi stops _MSC_VER from being defined
// - Passing -fms-compatibility stops __GNUC__ from being defined
// Neither being defined is a problem for things like things like MinGW's
// vadefs.h, which will fail during preprocessing if neither are defined.
// So, when targeting the GNU abi, we need to force __GNUC__ to be defined.
//
// TODO: This is a workaround that should be removed if possible.
if (comp.getTarget().isGnu()) {
// This is the same default gnuc version that Clang uses:
// https://github.com/llvm/llvm-project/blob/4b5366c9512aa273a5272af1d833961e1ed156e7/clang/lib/Driver/ToolChains/Clang.cpp#L6738
try argv.append("-fgnuc-version=4.2.1");
}
for (options.extra_include_paths.items) |extra_include_path| {
try argv.append("--include-directory");
try argv.append(extra_include_path);
}
var symbol_it = options.symbols.iterator();
while (symbol_it.next()) |entry| {
switch (entry.value_ptr.*) {
.define => |value| {
try argv.append("-D");
const define_arg = arg: {
const arg = try std.fmt.allocPrint(comp.gpa, "{s}={s}", .{ entry.key_ptr.*, value });
errdefer comp.gpa.free(arg);
try temp_strings.append(arg);
break :arg arg;
};
try argv.append(define_arg);
},
.undefine => {
try argv.append("-U");
try argv.append(entry.key_ptr.*);
},
}
}

try argv.append(win32_resource.src.src_path);
try argv.appendSlice(&[_][]const u8{
"-o",
267 changes: 267 additions & 0 deletions src/main.zig
Original file line number Diff line number Diff line change
@@ -104,6 +104,7 @@ const normal_usage =
\\ lib Use Zig as a drop-in lib.exe
\\ ranlib Use Zig as a drop-in ranlib
\\ objcopy Use Zig as a drop-in objcopy
\\ rc Use Zig as a drop-in rc.exe
\\
\\ env Print lib path, std path, cache directory, and version
\\ help Print this help and exit
@@ -300,6 +301,8 @@ pub fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi
return buildOutputType(gpa, arena, args, .cpp);
} else if (mem.eql(u8, cmd, "translate-c")) {
return buildOutputType(gpa, arena, args, .translate_c);
} else if (mem.eql(u8, cmd, "rc")) {
return cmdRc(gpa, arena, args[1..]);
} else if (mem.eql(u8, cmd, "fmt")) {
return cmdFmt(gpa, arena, cmd_args);
} else if (mem.eql(u8, cmd, "objcopy")) {
@@ -4372,6 +4375,270 @@ fn cmdTranslateC(comp: *Compilation, arena: Allocator, fancy_output: ?*Compilati
}
}

fn cmdRc(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
const resinator = @import("resinator.zig");

const stderr = std.io.getStdErr();
const stderr_config = std.io.tty.detectConfig(stderr);

var options = options: {
var cli_diagnostics = resinator.cli.Diagnostics.init(gpa);
defer cli_diagnostics.deinit();
var options = resinator.cli.parse(gpa, args, &cli_diagnostics) catch |err| switch (err) {
error.ParseError => {
cli_diagnostics.renderToStdErr(args, stderr_config);
process.exit(1);
},
else => |e| return e,
};
try options.maybeAppendRC(std.fs.cwd());

// print any warnings/notes
cli_diagnostics.renderToStdErr(args, stderr_config);
// If there was something printed, then add an extra newline separator
// so that there is a clear separation between the cli diagnostics and whatever
// gets printed after
if (cli_diagnostics.errors.items.len > 0) {
std.debug.print("\n", .{});
}
break :options options;
};
defer options.deinit();

if (options.print_help_and_exit) {
try resinator.cli.writeUsage(stderr.writer(), "zig rc");
return;
}

const stdout_writer = std.io.getStdOut().writer();
if (options.verbose) {
try options.dumpVerbose(stdout_writer);
try stdout_writer.writeByte('\n');
}

var full_input = full_input: {
if (options.preprocess != .no) {
if (!build_options.have_llvm) {
fatal("clang not available: compiler built without LLVM extensions", .{});
}

var argv = std.ArrayList([]const u8).init(gpa);
defer argv.deinit();

const self_exe_path = try introspect.findZigExePath(arena);
var zig_lib_directory = introspect.findZigLibDirFromSelfExe(arena, self_exe_path) catch |err| {
try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to find zig installation directory: {s}", .{@errorName(err)});
process.exit(1);
};
defer zig_lib_directory.handle.close();

const include_args = detectRcIncludeDirs(arena, zig_lib_directory.path.?, options.auto_includes) catch |err| {
try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to detect system include directories: {s}", .{@errorName(err)});
process.exit(1);
};

try argv.appendSlice(&[_][]const u8{ self_exe_path, "clang" });

const clang_target = clang_target: {
if (include_args.target_abi) |abi| {
break :clang_target try std.fmt.allocPrint(arena, "x86_64-unknown-windows-{s}", .{abi});
}
break :clang_target "x86_64-unknown-windows";
};
try resinator.preprocess.appendClangArgs(arena, &argv, options, .{
.clang_target = clang_target,
.system_include_paths = include_args.include_paths,
.needs_gnu_workaround = if (include_args.target_abi) |abi| std.mem.eql(u8, abi, "gnu") else false,
.nostdinc = true,
});

try argv.append(options.input_filename);

if (options.verbose) {
try stdout_writer.writeAll("Preprocessor: zig clang\n");
for (argv.items[0 .. argv.items.len - 1]) |arg| {
try stdout_writer.print("{s} ", .{arg});
}
try stdout_writer.print("{s}\n\n", .{argv.items[argv.items.len - 1]});
}

if (std.process.can_spawn) {
var result = std.ChildProcess.exec(.{
.allocator = gpa,
.argv = argv.items,
.max_output_bytes = std.math.maxInt(u32),
}) catch |err| {
try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to spawn preprocessor child process: {s}", .{@errorName(err)});
process.exit(1);
};
errdefer gpa.free(result.stdout);
defer gpa.free(result.stderr);

switch (result.term) {
.Exited => |code| {
if (code != 0) {
try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "the preprocessor failed with exit code {}:", .{code});
try stderr.writeAll(result.stderr);
try stderr.writeAll("\n");
process.exit(1);
}
},
.Signal, .Stopped, .Unknown => {
try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "the preprocessor terminated unexpectedly ({s}):", .{@tagName(result.term)});
try stderr.writeAll(result.stderr);
try stderr.writeAll("\n");
process.exit(1);
},
}

break :full_input result.stdout;
} else {
// need to use an intermediate file
const rand_int = std.crypto.random.int(u64);
const preprocessed_path = try std.fmt.allocPrint(gpa, "resinator{x}.rcpp", .{rand_int});
defer gpa.free(preprocessed_path);
defer std.fs.cwd().deleteFile(preprocessed_path) catch {};

try argv.appendSlice(&.{ "-o", preprocessed_path });
const exit_code = try clangMain(arena, argv.items);
if (exit_code != 0) {
try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "the preprocessor failed with exit code {}:", .{exit_code});
process.exit(1);
}
break :full_input std.fs.cwd().readFileAlloc(gpa, preprocessed_path, std.math.maxInt(usize)) catch |err| {
try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to read preprocessed file path '{s}': {s}", .{ preprocessed_path, @errorName(err) });
process.exit(1);
};
}
} else {
break :full_input std.fs.cwd().readFileAlloc(gpa, options.input_filename, std.math.maxInt(usize)) catch |err| {
try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to read input file path '{s}': {s}", .{ options.input_filename, @errorName(err) });
process.exit(1);
};
}
};
defer gpa.free(full_input);

if (options.preprocess == .only) {
std.fs.cwd().writeFile(options.output_filename, full_input) catch |err| {
try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to write output file '{s}': {s}", .{ options.output_filename, @errorName(err) });
process.exit(1);
};
return cleanExit();
}

var mapping_results = try resinator.source_mapping.parseAndRemoveLineCommands(gpa, full_input, full_input, .{ .initial_filename = options.input_filename });
defer mapping_results.mappings.deinit(gpa);

var final_input = resinator.comments.removeComments(mapping_results.result, mapping_results.result, &mapping_results.mappings);

var output_file = std.fs.cwd().createFile(options.output_filename, .{}) catch |err| {
try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to create output file '{s}': {s}", .{ options.output_filename, @errorName(err) });
process.exit(1);
};
var output_file_closed = false;
defer if (!output_file_closed) output_file.close();

var diagnostics = resinator.errors.Diagnostics.init(gpa);
defer diagnostics.deinit();

var output_buffered_stream = std.io.bufferedWriter(output_file.writer());

resinator.compile.compile(gpa, final_input, output_buffered_stream.writer(), .{
.cwd = std.fs.cwd(),
.diagnostics = &diagnostics,
.source_mappings = &mapping_results.mappings,
.dependencies_list = null,
.ignore_include_env_var = options.ignore_include_env_var,
.extra_include_paths = options.extra_include_paths.items,
.default_language_id = options.default_language_id,
.default_code_page = options.default_code_page orelse .windows1252,
.verbose = options.verbose,
.null_terminate_string_table_strings = options.null_terminate_string_table_strings,
.max_string_literal_codepoints = options.max_string_literal_codepoints,
.silent_duplicate_control_ids = options.silent_duplicate_control_ids,
.warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page,
}) catch |err| switch (err) {
error.ParseError, error.CompileError => {
diagnostics.renderToStdErr(std.fs.cwd(), final_input, stderr_config, mapping_results.mappings);
// Delete the output file on error
output_file.close();
output_file_closed = true;
// Failing to delete is not really a big deal, so swallow any errors
std.fs.cwd().deleteFile(options.output_filename) catch {};
process.exit(1);
},
else => |e| return e,
};

try output_buffered_stream.flush();

// print any warnings/notes
diagnostics.renderToStdErr(std.fs.cwd(), final_input, stderr_config, mapping_results.mappings);

return cleanExit();
}

const RcIncludeArgs = struct {
include_paths: []const []const u8 = &.{},
target_abi: ?[]const u8 = null,
};

fn detectRcIncludeDirs(arena: Allocator, zig_lib_dir: []const u8, auto_includes: @import("resinator.zig").cli.Options.AutoIncludes) !RcIncludeArgs {
if (auto_includes == .none) return .{};
var cur_includes = auto_includes;
if (builtin.target.os.tag != .windows) {
switch (cur_includes) {
// MSVC can't be found when the host isn't Windows, so short-circuit.
.msvc => return error.WindowsSdkNotFound,
// Skip straight to gnu since we won't be able to detect MSVC on non-Windows hosts.
.any => cur_includes = .gnu,
.gnu => {},
.none => unreachable,
}
}
while (true) {
switch (cur_includes) {
.any, .msvc => {
const cross_target = std.zig.CrossTarget.parse(.{ .arch_os_abi = "native-windows-msvc" }) catch unreachable;
const target = cross_target.toTarget();
const is_native_abi = cross_target.isNativeAbi();
const detected_libc = Compilation.detectLibCIncludeDirs(arena, zig_lib_dir, target, is_native_abi, true, null) catch |err| {
if (cur_includes == .any) {
// fall back to mingw
cur_includes = .gnu;
continue;
}
return err;
};
if (detected_libc.libc_include_dir_list.len == 0) {
if (cur_includes == .any) {
// fall back to mingw
cur_includes = .gnu;
continue;
}
return error.WindowsSdkNotFound;
}
return .{
.include_paths = detected_libc.libc_include_dir_list,
.target_abi = "msvc",
};
},
.gnu => {
const cross_target = std.zig.CrossTarget.parse(.{ .arch_os_abi = "native-windows-gnu" }) catch unreachable;
const target = cross_target.toTarget();
const is_native_abi = cross_target.isNativeAbi();
const detected_libc = try Compilation.detectLibCIncludeDirs(arena, zig_lib_dir, target, is_native_abi, true, null);
return .{
.include_paths = detected_libc.libc_include_dir_list,
.target_abi = "gnu",
};
},
.none => unreachable,
}
}
}

pub const usage_libc =
\\Usage: zig libc
\\
1 change: 1 addition & 0 deletions src/resinator.zig
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ pub const lang = @import("resinator/lang.zig");
pub const lex = @import("resinator/lex.zig");
pub const literals = @import("resinator/literals.zig");
pub const parse = @import("resinator/parse.zig");
pub const preprocess = @import("resinator/preprocess.zig");
pub const rc = @import("resinator/rc.zig");
pub const res = @import("resinator/res.zig");
pub const source_mapping = @import("resinator/source_mapping.zig");
10 changes: 8 additions & 2 deletions src/resinator/cli.zig
Original file line number Diff line number Diff line change
@@ -8,8 +8,8 @@ const lex = @import("lex.zig");
/// This is what /SL 100 will set the maximum string literal length to
pub const max_string_literal_length_100_percent = 8192;

pub const usage_string =
\\Usage: resinator [options] [--] <INPUT> [<OUTPUT>]
pub const usage_string_after_command_name =
\\ [options] [--] <INPUT> [<OUTPUT>]
\\
\\The sequence -- can be used to signify when to stop parsing options.
\\This is necessary when the input path begins with a forward slash.
@@ -57,6 +57,12 @@ pub const usage_string =
\\
;

pub fn writeUsage(writer: anytype, command_name: []const u8) !void {
try writer.writeAll("Usage: ");
try writer.writeAll(command_name);
try writer.writeAll(usage_string_after_command_name);
}

pub const Diagnostics = struct {
errors: std.ArrayListUnmanaged(ErrorDetails) = .{},
allocator: Allocator,
Loading

0 comments on commit 2769215

Please sign in to comment.