Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 8 additions & 53 deletions src/Compilation.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4616,63 +4616,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",
Expand Down
267 changes: 267 additions & 0 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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..]);
Comment on lines +304 to +305
Copy link
Member Author

@squeek502 squeek502 Oct 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note:

    } else if (build_options.only_core_functionality) {
        @panic("only a few subcommands are supported in a zig2.c build");
    }

above takes care of ensuring resinator is not included in the build when only_core_functionality is true.

} else if (mem.eql(u8, cmd, "fmt")) {
return cmdFmt(gpa, arena, cmd_args);
} else if (mem.eql(u8, cmd, "objcopy")) {
Expand Down Expand Up @@ -4354,6 +4357,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
\\
Expand Down
1 change: 1 addition & 0 deletions src/resinator.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
10 changes: 8 additions & 2 deletions src/resinator/cli.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
Loading