diff --git a/src/Compilation.zig b/src/Compilation.zig index 25d0d63b1eee..4f26c95ea948 100644 --- a/src/Compilation.zig +++ b/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 instead of # - "-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", diff --git a/src/main.zig b/src/main.zig index 9199fe205b42..14d187796c8d 100644 --- a/src/main.zig +++ b/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 \\ diff --git a/src/resinator.zig b/src/resinator.zig index 698d92e729bf..1d7e75fec026 100644 --- a/src/resinator.zig +++ b/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"); diff --git a/src/resinator/cli.zig b/src/resinator/cli.zig index 2e244b878e48..50dbed5ad69d 100644 --- a/src/resinator/cli.zig +++ b/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] [--] [] +pub const usage_string_after_command_name = + \\ [options] [--] [] \\ \\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, diff --git a/src/resinator/preprocess.zig b/src/resinator/preprocess.zig new file mode 100644 index 000000000000..e831c8147cf0 --- /dev/null +++ b/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 instead of # + // 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.*); + }, + } + } +} diff --git a/src/resinator/utils.zig b/src/resinator/utils.zig index a29f068aeaf8..41f504867be0 100644 --- a/src/resinator/utils.zig +++ b/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); +}