Commit 2769215b90
Changed files (6)
src/resinator/cli.zig
@@ -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,
src/resinator/preprocess.zig
@@ -0,0 +1,94 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const cli = @import("cli.zig");
+
+pub const IncludeArgs = struct {
+ clang_target: ?[]const u8 = null,
+ system_include_paths: []const []const u8,
+ /// Should be set to `true` when -target has the GNU abi
+ /// (either because `clang_target` has `-gnu` or `-target`
+ /// is appended via other means and it has `-gnu`)
+ needs_gnu_workaround: bool = false,
+ nostdinc: bool = false,
+
+ pub const IncludeAbi = enum {
+ msvc,
+ gnu,
+ };
+};
+
+/// `arena` is used for temporary -D argument strings and the INCLUDE environment variable.
+/// The arena should be kept alive at least as long as `argv`.
+pub fn appendClangArgs(arena: Allocator, argv: *std.ArrayList([]const u8), options: cli.Options, include_args: IncludeArgs) !void {
+ try argv.appendSlice(&[_][]const u8{
+ "-E", // preprocessor only
+ "--comments",
+ "-fuse-line-directives", // #line <num> instead of # <num>
+ // TODO: could use --trace-includes to give info about what's included from where
+ "-xc", // output c
+ // TODO: Turn this off, check the warnings, and convert the spaces back to NUL
+ "-Werror=null-character", // error on null characters instead of converting them to spaces
+ // TODO: could remove -Werror=null-character and instead parse warnings looking for 'warning: null character ignored'
+ // since the only real problem is when clang doesn't preserve null characters
+ //"-Werror=invalid-pp-token", // will error on unfinished string literals
+ // TODO: could use -Werror instead
+ "-fms-compatibility", // Allow things like "header.h" to be resolved relative to the 'root' .rc file, among other things
+ // https://learn.microsoft.com/en-us/windows/win32/menurc/predefined-macros
+ "-DRC_INVOKED",
+ });
+ for (options.extra_include_paths.items) |extra_include_path| {
+ try argv.append("-I");
+ try argv.append(extra_include_path);
+ }
+
+ if (include_args.nostdinc) {
+ try argv.append("-nostdinc");
+ }
+ for (include_args.system_include_paths) |include_path| {
+ try argv.append("-isystem");
+ try argv.append(include_path);
+ }
+ if (include_args.clang_target) |target| {
+ try argv.append("-target");
+ try argv.append(target);
+ }
+ // 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 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 (include_args.needs_gnu_workaround) {
+ // 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");
+ }
+
+ if (!options.ignore_include_env_var) {
+ const INCLUDE = std.process.getEnvVarOwned(arena, "INCLUDE") catch "";
+
+ // TODO: Should this be platform-specific? How does windres/llvm-rc handle this (if at all)?
+ var it = std.mem.tokenize(u8, INCLUDE, ";");
+ while (it.next()) |include_path| {
+ try argv.append("-isystem");
+ try argv.append(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 = try std.fmt.allocPrint(arena, "{s}={s}", .{ entry.key_ptr.*, value });
+ try argv.append(define_arg);
+ },
+ .undefine => {
+ try argv.append("-U");
+ try argv.append(entry.key_ptr.*);
+ },
+ }
+ }
+}
src/resinator/utils.zig
@@ -81,3 +81,32 @@ pub fn isNonAsciiDigit(c: u21) bool {
else => false,
};
}
+
+/// Used for generic colored errors/warnings/notes, more context-specific error messages
+/// are handled elsewhere.
+pub fn renderErrorMessage(writer: anytype, config: std.io.tty.Config, msg_type: enum { err, warning, note }, comptime format: []const u8, args: anytype) !void {
+ switch (msg_type) {
+ .err => {
+ try config.setColor(writer, .bold);
+ try config.setColor(writer, .red);
+ try writer.writeAll("error: ");
+ },
+ .warning => {
+ try config.setColor(writer, .bold);
+ try config.setColor(writer, .yellow);
+ try writer.writeAll("warning: ");
+ },
+ .note => {
+ try config.setColor(writer, .reset);
+ try config.setColor(writer, .cyan);
+ try writer.writeAll("note: ");
+ },
+ }
+ try config.setColor(writer, .reset);
+ if (msg_type == .err) {
+ try config.setColor(writer, .bold);
+ }
+ try writer.print(format, args);
+ try writer.writeByte('\n');
+ try config.setColor(writer, .reset);
+}
src/Compilation.zig
@@ -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",
src/main.zig
@@ -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
\\
src/resinator.zig
@@ -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");