diff options
Diffstat (limited to 'src/Compilation.zig')
| -rw-r--r-- | src/Compilation.zig | 771 |
1 files changed, 763 insertions, 8 deletions
diff --git a/src/Compilation.zig b/src/Compilation.zig index 177c71f4d1..57504d97ab 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -39,6 +39,7 @@ const libtsan = @import("libtsan.zig"); const Zir = @import("Zir.zig"); const Autodoc = @import("Autodoc.zig"); const Color = @import("main.zig").Color; +const resinator = @import("resinator.zig"); /// General-purpose allocator. Used for both temporary and long-term storage. gpa: Allocator, @@ -46,6 +47,7 @@ gpa: Allocator, arena_state: std.heap.ArenaAllocator.State, bin_file: *link.File, c_object_table: std.AutoArrayHashMapUnmanaged(*CObject, void) = .{}, +win32_resource_table: std.AutoArrayHashMapUnmanaged(*Win32Resource, void) = .{}, /// This is a pointer to a local variable inside `update()`. whole_cache_manifest: ?*Cache.Manifest = null, whole_cache_manifest_mutex: std.Thread.Mutex = .{}, @@ -60,6 +62,10 @@ anon_work_queue: std.fifo.LinearFifo(Job, .Dynamic), /// gets linked with the Compilation. c_object_work_queue: std.fifo.LinearFifo(*CObject, .Dynamic), +/// These jobs are to invoke the RC compiler to create a compiled resource file (.res), which +/// gets linked with the Compilation. +win32_resource_work_queue: std.fifo.LinearFifo(*Win32Resource, .Dynamic), + /// These jobs are to tokenize, parse, and astgen files, which may be outdated /// since the last compilation, as well as scan for `@import` and queue up /// additional jobs corresponding to those new files. @@ -73,6 +79,10 @@ embed_file_work_queue: std.fifo.LinearFifo(*Module.EmbedFile, .Dynamic), /// This data is accessed by multiple threads and is protected by `mutex`. failed_c_objects: std.AutoArrayHashMapUnmanaged(*CObject, *CObject.ErrorMsg) = .{}, +/// The ErrorBundle memory is owned by the `Win32Resource`, using Compilation's general purpose allocator. +/// This data is accessed by multiple threads and is protected by `mutex`. +failed_win32_resources: std.AutoArrayHashMapUnmanaged(*Win32Resource, ErrorBundle) = .{}, + /// Miscellaneous things that can fail. misc_failures: std.AutoArrayHashMapUnmanaged(MiscTask, MiscError) = .{}, @@ -109,6 +119,7 @@ last_update_was_cache_hit: bool = false, c_source_files: []const CSourceFile, clang_argv: []const []const u8, +rc_source_files: []const RcSourceFile, cache_parent: *Cache, /// Path to own executable for invoking `zig clang`. self_exe_path: ?[]const u8, @@ -125,6 +136,7 @@ local_cache_directory: Directory, global_cache_directory: Directory, libc_include_dir_list: []const []const u8, libc_framework_dir_list: []const []const u8, +rc_include_dir_list: []const []const u8, thread_pool: *ThreadPool, /// Populated when we build the libc++ static library. A Job to build this is placed in the queue @@ -225,6 +237,23 @@ pub const CSourceFile = struct { ext: ?FileExt = null, }; +/// For passing to resinator. +pub const RcSourceFile = struct { + src_path: []const u8, + extra_flags: []const []const u8 = &.{}, +}; + +pub const RcIncludes = enum { + /// Use MSVC if available, fall back to MinGW. + any, + /// Use MSVC include paths (MSVC install + Windows SDK, must be present on the system). + msvc, + /// Use MinGW include paths (distributed with Zig). + gnu, + /// Do not use any autodetected include paths. + none, +}; + const Job = union(enum) { /// Write the constant value for a Decl to the output file. codegen_decl: Module.Decl.Index, @@ -326,6 +355,50 @@ pub const CObject = struct { } }; +pub const Win32Resource = struct { + /// Relative to cwd. Owned by arena. + src: RcSourceFile, + status: union(enum) { + new, + success: struct { + /// The outputted result. Owned by gpa. + res_path: []u8, + /// This is a file system lock on the cache hash manifest representing this + /// object. It prevents other invocations of the Zig compiler from interfering + /// with this object until released. + lock: Cache.Lock, + }, + /// There will be a corresponding ErrorMsg in Compilation.failed_win32_resources. + failure, + /// A transient failure happened when trying to compile the resource file; it may + /// succeed if we try again. There may be a corresponding ErrorMsg in + /// Compilation.failed_win32_resources. If there is not, the failure is out of memory. + failure_retryable, + }, + + /// Returns true if there was failure. + pub fn clearStatus(self: *Win32Resource, gpa: Allocator) bool { + switch (self.status) { + .new => return false, + .failure, .failure_retryable => { + self.status = .new; + return true; + }, + .success => |*success| { + gpa.free(success.res_path); + success.lock.release(); + self.status = .new; + return false; + }, + } + } + + pub fn destroy(self: *Win32Resource, gpa: Allocator) void { + _ = self.clearStatus(gpa); + gpa.destroy(self); + } +}; + pub const MiscTask = enum { write_builtin_zig, glibc_crt_file, @@ -505,6 +578,8 @@ pub const InitOptions = struct { rpath_list: []const []const u8 = &[0][]const u8{}, symbol_wrap_set: std.StringArrayHashMapUnmanaged(void) = .{}, c_source_files: []const CSourceFile = &[0]CSourceFile{}, + rc_source_files: []const RcSourceFile = &[0]RcSourceFile{}, + rc_includes: RcIncludes = .any, link_objects: []LinkObject = &[0]LinkObject{}, framework_dirs: []const []const u8 = &[0][]const u8{}, frameworks: []const Framework = &.{}, @@ -938,6 +1013,11 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation { options.libc_installation, ); + const rc_dirs = try detectWin32ResourceIncludeDirs( + arena, + options, + ); + const sysroot = options.sysroot orelse libc_dirs.sysroot; const must_pie = target_util.requiresPIE(options.target); @@ -1591,16 +1671,19 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation { .work_queue = std.fifo.LinearFifo(Job, .Dynamic).init(gpa), .anon_work_queue = std.fifo.LinearFifo(Job, .Dynamic).init(gpa), .c_object_work_queue = std.fifo.LinearFifo(*CObject, .Dynamic).init(gpa), + .win32_resource_work_queue = std.fifo.LinearFifo(*Win32Resource, .Dynamic).init(gpa), .astgen_work_queue = std.fifo.LinearFifo(*Module.File, .Dynamic).init(gpa), .embed_file_work_queue = std.fifo.LinearFifo(*Module.EmbedFile, .Dynamic).init(gpa), .keep_source_files_loaded = options.keep_source_files_loaded, .use_clang = use_clang, .clang_argv = options.clang_argv, .c_source_files = options.c_source_files, + .rc_source_files = options.rc_source_files, .cache_parent = cache, .self_exe_path = options.self_exe_path, .libc_include_dir_list = libc_dirs.libc_include_dir_list, .libc_framework_dir_list = libc_dirs.libc_framework_dir_list, + .rc_include_dir_list = rc_dirs.libc_include_dir_list, .sanitize_c = sanitize_c, .thread_pool = options.thread_pool, .clang_passthrough_mode = options.clang_passthrough_mode, @@ -1647,6 +1730,19 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation { comp.c_object_table.putAssumeCapacityNoClobber(c_object, {}); } + // Add a `Win32Resource` for each `rc_source_files`. + try comp.win32_resource_table.ensureTotalCapacity(gpa, options.rc_source_files.len); + for (options.rc_source_files) |rc_source_file| { + const win32_resource = try gpa.create(Win32Resource); + errdefer gpa.destroy(win32_resource); + + win32_resource.* = .{ + .status = .{ .new = {} }, + .src = rc_source_file, + }; + comp.win32_resource_table.putAssumeCapacityNoClobber(win32_resource, {}); + } + const have_bin_emit = comp.bin_file.options.emit != null or comp.whole_bin_sub_path != null; if (have_bin_emit and !comp.bin_file.options.skip_linker_dependencies and target.ofmt != .c) { @@ -1804,6 +1900,7 @@ pub fn destroy(self: *Compilation) void { self.work_queue.deinit(); self.anon_work_queue.deinit(); self.c_object_work_queue.deinit(); + self.win32_resource_work_queue.deinit(); self.astgen_work_queue.deinit(); self.embed_file_work_queue.deinit(); @@ -1852,6 +1949,16 @@ pub fn destroy(self: *Compilation) void { } self.failed_c_objects.deinit(gpa); + for (self.win32_resource_table.keys()) |key| { + key.destroy(gpa); + } + self.win32_resource_table.deinit(gpa); + + for (self.failed_win32_resources.values()) |*value| { + value.deinit(gpa); + } + self.failed_win32_resources.deinit(gpa); + for (self.lld_errors.items) |*lld_error| { lld_error.deinit(gpa); } @@ -2014,6 +2121,13 @@ pub fn update(comp: *Compilation, main_progress_node: *std.Progress.Node) !void comp.c_object_work_queue.writeItemAssumeCapacity(key); } + // For compiling Win32 resources, we rely on the cache hash system to avoid duplicating work. + // Add a Job for each Win32 resource file. + try comp.win32_resource_work_queue.ensureUnusedCapacity(comp.win32_resource_table.count()); + for (comp.win32_resource_table.keys()) |key| { + comp.win32_resource_work_queue.writeItemAssumeCapacity(key); + } + if (comp.bin_file.options.module) |module| { module.compile_log_text.shrinkAndFree(module.gpa, 0); module.generation += 1; @@ -2336,6 +2450,13 @@ fn addNonIncrementalStuffToCacheManifest(comp: *Compilation, man: *Cache.Manifes man.hash.addListOfBytes(key.src.extra_flags); } + for (comp.win32_resource_table.keys()) |key| { + _ = try man.addFile(key.src.src_path, null); + man.hash.addListOfBytes(key.src.extra_flags); + } + + man.hash.addListOfBytes(comp.rc_include_dir_list); + cache_helpers.addOptionalEmitLoc(&man.hash, comp.emit_asm); cache_helpers.addOptionalEmitLoc(&man.hash, comp.emit_llvm_ir); cache_helpers.addOptionalEmitLoc(&man.hash, comp.emit_llvm_bc); @@ -2571,8 +2692,14 @@ pub fn makeBinFileWritable(self: *Compilation) !void { /// This function is temporally single-threaded. pub fn totalErrorCount(self: *Compilation) u32 { - var total: usize = self.failed_c_objects.count() + self.misc_failures.count() + - @intFromBool(self.alloc_failure_occurred) + self.lld_errors.items.len; + var total: usize = self.failed_c_objects.count() + + self.misc_failures.count() + + @intFromBool(self.alloc_failure_occurred) + + self.lld_errors.items.len; + + for (self.failed_win32_resources.values()) |errs| { + total += errs.errorMessageCount(); + } if (self.bin_file.options.module) |module| { total += module.failed_exports.count(); @@ -2664,6 +2791,13 @@ pub fn getAllErrorsAlloc(self: *Compilation) !ErrorBundle { } } + { + var it = self.failed_win32_resources.iterator(); + while (it.next()) |entry| { + try bundle.addBundleAsRoots(entry.value_ptr.*); + } + } + for (self.lld_errors.items) |lld_error| { const notes_len = @as(u32, @intCast(lld_error.context_lines.len)); @@ -2683,7 +2817,7 @@ pub fn getAllErrorsAlloc(self: *Compilation) !ErrorBundle { .msg = try bundle.addString(value.msg), .notes_len = if (value.children) |b| b.errorMessageCount() else 0, }); - if (value.children) |b| try bundle.addBundle(b); + if (value.children) |b| try bundle.addBundleAsNotes(b); } if (self.alloc_failure_occurred) { try bundle.addRootErrorMessage(.{ @@ -3082,6 +3216,9 @@ pub fn performAllTheWork( var c_obj_prog_node = main_progress_node.start("Compile C Objects", comp.c_source_files.len); defer c_obj_prog_node.end(); + var win32_resource_prog_node = main_progress_node.start("Compile Win32 Resources", comp.rc_source_files.len); + defer win32_resource_prog_node.end(); + var embed_file_prog_node = main_progress_node.start("Detect @embedFile updates", comp.embed_file_work_queue.count); defer embed_file_prog_node.end(); @@ -3130,6 +3267,13 @@ pub fn performAllTheWork( comp, c_object, &c_obj_prog_node, &comp.work_queue_wait_group, }); } + + while (comp.win32_resource_work_queue.readItem()) |win32_resource| { + comp.work_queue_wait_group.start(); + try comp.thread_pool.spawn(workerUpdateWin32Resource, .{ + comp, win32_resource, &win32_resource_prog_node, &comp.work_queue_wait_group, + }); + } } if (comp.bin_file.options.module) |mod| { @@ -3659,6 +3803,14 @@ pub fn obtainCObjectCacheManifest(comp: *const Compilation) Cache.Manifest { return man; } +pub fn obtainWin32ResourceCacheManifest(comp: *const Compilation) Cache.Manifest { + var man = comp.cache_parent.obtain(); + + man.hash.addListOfBytes(comp.rc_include_dir_list); + + return man; +} + test "cImport" { _ = cImport; } @@ -3832,6 +3984,26 @@ fn workerUpdateCObject( }; } +fn workerUpdateWin32Resource( + comp: *Compilation, + win32_resource: *Win32Resource, + progress_node: *std.Progress.Node, + wg: *WaitGroup, +) void { + defer wg.finish(); + + comp.updateWin32Resource(win32_resource, progress_node) catch |err| switch (err) { + error.AnalysisFail => return, + else => { + comp.reportRetryableWin32ResourceError(win32_resource, err) catch |oom| switch (oom) { + // Swallowing this error is OK because it's implied to be OOM when + // there is a missing failed_win32_resources error message. + error.OutOfMemory => {}, + }; + }, + }; +} + fn buildCompilerRtOneShot( comp: *Compilation, output_mode: std.builtin.OutputMode, @@ -3877,6 +4049,18 @@ fn reportRetryableCObjectError( } } +fn reportRetryableWin32ResourceError( + comp: *Compilation, + win32_resource: *Win32Resource, + err: anyerror, +) error{OutOfMemory}!void { + win32_resource.status = .failure_retryable; + + // TODO: something + _ = comp; + _ = @errorName(err); +} + fn reportRetryableAstGenError( comp: *Compilation, src: AstGenSrc, @@ -4233,6 +4417,311 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: *std.P }; } +fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32_resource_prog_node: *std.Progress.Node) !void { + if (!build_options.have_llvm) { + return comp.failWin32Resource(win32_resource, "clang not available: compiler built without LLVM extensions", .{}); + } + const self_exe_path = comp.self_exe_path orelse + return comp.failWin32Resource(win32_resource, "clang compilation disabled", .{}); + + const tracy_trace = trace(@src()); + defer tracy_trace.end(); + + log.debug("updating win32 resource: {s}", .{win32_resource.src.src_path}); + + if (win32_resource.clearStatus(comp.gpa)) { + // There was previous failure. + comp.mutex.lock(); + defer comp.mutex.unlock(); + // If the failure was OOM, there will not be an entry here, so we do + // not assert discard. + _ = comp.failed_win32_resources.swapRemove(win32_resource); + } + + var man = comp.obtainWin32ResourceCacheManifest(); + defer man.deinit(); + + _ = try man.addFile(win32_resource.src.src_path, null); + man.hash.addListOfBytes(win32_resource.src.extra_flags); + + var arena_allocator = std.heap.ArenaAllocator.init(comp.gpa); + defer arena_allocator.deinit(); + const arena = arena_allocator.allocator(); + + const rc_basename = std.fs.path.basename(win32_resource.src.src_path); + + win32_resource_prog_node.activate(); + var child_progress_node = win32_resource_prog_node.start(rc_basename, 0); + child_progress_node.activate(); + defer child_progress_node.end(); + + const rc_basename_noext = rc_basename[0 .. rc_basename.len - std.fs.path.extension(rc_basename).len]; + + const digest = if (try man.hit()) man.final() else blk: { + const rcpp_filename = try std.fmt.allocPrint(arena, "{s}.rcpp", .{rc_basename_noext}); + + const out_rcpp_path = try comp.tmpFilePath(arena, rcpp_filename); + var zig_cache_tmp_dir = try comp.local_cache_directory.handle.makeOpenPath("tmp", .{}); + defer zig_cache_tmp_dir.close(); + + const res_filename = try std.fmt.allocPrint(arena, "{s}.res", .{rc_basename_noext}); + + // We can't know the digest until we do the compilation, + // so we need a temporary filename. + const out_res_path = try comp.tmpFilePath(arena, res_filename); + + var options = options: { + var resinator_args = try std.ArrayListUnmanaged([]const u8).initCapacity(comp.gpa, win32_resource.src.extra_flags.len + 4); + defer resinator_args.deinit(comp.gpa); + + resinator_args.appendAssumeCapacity(""); // dummy 'process name' arg + resinator_args.appendSliceAssumeCapacity(win32_resource.src.extra_flags); + resinator_args.appendSliceAssumeCapacity(&.{ "--", out_rcpp_path, out_res_path }); + + var cli_diagnostics = resinator.cli.Diagnostics.init(comp.gpa); + defer cli_diagnostics.deinit(); + var options = resinator.cli.parse(comp.gpa, resinator_args.items, &cli_diagnostics) catch |err| switch (err) { + error.ParseError => { + return comp.failWin32ResourceCli(win32_resource, &cli_diagnostics); + }, + else => |e| return e, + }; + break :options options; + }; + defer options.deinit(); + + 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 + }); + // 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", + out_rcpp_path, + }); + + const out_dep_path = try std.fmt.allocPrint(arena, "{s}.d", .{out_rcpp_path}); + // Note: addCCArgs will implicitly add _DEBUG/NDEBUG depending on the optimization + // mode. While these defines are not normally present when calling rc.exe directly, + // them being defined matches the behavior of how MSVC calls rc.exe which is the more + // relevant behavior in this case. + try comp.addCCArgs(arena, &argv, .rc, out_dep_path); + + if (comp.verbose_cc) { + dump_argv(argv.items); + } + + if (std.process.can_spawn) { + var child = std.ChildProcess.init(argv.items, arena); + child.stdin_behavior = .Ignore; + child.stdout_behavior = .Ignore; + child.stderr_behavior = .Pipe; + + try child.spawn(); + + const stderr_reader = child.stderr.?.reader(); + + const stderr = try stderr_reader.readAllAlloc(arena, 10 * 1024 * 1024); + + const term = child.wait() catch |err| { + return comp.failWin32Resource(win32_resource, "unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) }); + }; + + switch (term) { + .Exited => |code| { + if (code != 0) { + // TODO parse clang stderr and turn it into an error message + // and then call failCObjWithOwnedErrorMsg + log.err("clang preprocessor failed with stderr:\n{s}", .{stderr}); + return comp.failWin32Resource(win32_resource, "clang preprocessor exited with code {d}", .{code}); + } + }, + else => { + log.err("clang preprocessor terminated with stderr:\n{s}", .{stderr}); + return comp.failWin32Resource(win32_resource, "clang preprocessor terminated unexpectedly", .{}); + }, + } + } else { + const exit_code = try clangMain(arena, argv.items); + if (exit_code != 0) { + return comp.failWin32Resource(win32_resource, "clang preprocessor exited with code {d}", .{exit_code}); + } + } + + const dep_basename = std.fs.path.basename(out_dep_path); + // Add the files depended on to the cache system. + try man.addDepFilePost(zig_cache_tmp_dir, dep_basename); + if (comp.whole_cache_manifest) |whole_cache_manifest| { + comp.whole_cache_manifest_mutex.lock(); + defer comp.whole_cache_manifest_mutex.unlock(); + try whole_cache_manifest.addDepFilePost(zig_cache_tmp_dir, dep_basename); + } + // Just to save disk space, we delete the file because it is never needed again. + zig_cache_tmp_dir.deleteFile(dep_basename) catch |err| { + log.warn("failed to delete '{s}': {s}", .{ out_dep_path, @errorName(err) }); + }; + + var full_input = std.fs.cwd().readFileAlloc(arena, out_rcpp_path, std.math.maxInt(usize)) catch |err| switch (err) { + error.OutOfMemory => return error.OutOfMemory, + else => |e| { + return comp.failWin32Resource(win32_resource, "failed to read preprocessed file '{s}': {s}", .{ out_rcpp_path, @errorName(e) }); + }, + }; + + var mapping_results = try resinator.source_mapping.parseAndRemoveLineCommands(arena, full_input, full_input, .{ .initial_filename = win32_resource.src.src_path }); + defer mapping_results.mappings.deinit(arena); + + var final_input = resinator.comments.removeComments(mapping_results.result, mapping_results.result, &mapping_results.mappings); + + var output_file = zig_cache_tmp_dir.createFile(out_res_path, .{}) catch |err| { + return comp.failWin32Resource(win32_resource, "failed to create output file '{s}': {s}", .{ out_res_path, @errorName(err) }); + }; + var output_file_closed = false; + defer if (!output_file_closed) output_file.close(); + + var diagnostics = resinator.errors.Diagnostics.init(arena); + defer diagnostics.deinit(); + + var dependencies_list = std.ArrayList([]const u8).init(comp.gpa); + defer { + for (dependencies_list.items) |item| { + comp.gpa.free(item); + } + dependencies_list.deinit(); + } + + var output_buffered_stream = std.io.bufferedWriter(output_file.writer()); + + resinator.compile.compile(arena, final_input, output_buffered_stream.writer(), .{ + .cwd = std.fs.cwd(), + .diagnostics = &diagnostics, + .source_mappings = &mapping_results.mappings, + .dependencies_list = &dependencies_list, + .system_include_paths = comp.rc_include_dir_list, + .ignore_include_env_var = true, + // options + .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 => { + // 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 + zig_cache_tmp_dir.deleteFile(out_res_path) catch { + log.warn("failed to delete '{s}': {s}", .{ out_res_path, @errorName(err) }); + }; + return comp.failWin32ResourceCompile(win32_resource, final_input, &diagnostics, mapping_results.mappings); + }, + else => |e| return e, + }; + + try output_buffered_stream.flush(); + + for (dependencies_list.items) |dep_file_path| { + try man.addFilePost(dep_file_path); + if (comp.whole_cache_manifest) |whole_cache_manifest| { + comp.whole_cache_manifest_mutex.lock(); + defer comp.whole_cache_manifest_mutex.unlock(); + try whole_cache_manifest.addFilePost(dep_file_path); + } + } + + // Rename into place. + const digest = man.final(); + const o_sub_path = try std.fs.path.join(arena, &[_][]const u8{ "o", &digest }); + var o_dir = try comp.local_cache_directory.handle.makeOpenPath(o_sub_path, .{}); + defer o_dir.close(); + const tmp_basename = std.fs.path.basename(out_res_path); + try std.fs.rename(zig_cache_tmp_dir, tmp_basename, o_dir, res_filename); + const tmp_rcpp_basename = std.fs.path.basename(out_rcpp_path); + try std.fs.rename(zig_cache_tmp_dir, tmp_rcpp_basename, o_dir, rcpp_filename); + break :blk digest; + }; + + if (man.have_exclusive_lock) { + // Write the updated manifest. This is a no-op if the manifest is not dirty. Note that it is + // possible we had a hit and the manifest is dirty, for example if the file mtime changed but + // the contents were the same, we hit the cache but the manifest is dirty and we need to update + // it to prevent doing a full file content comparison the next time around. + man.writeManifest() catch |err| { + log.warn("failed to write cache manifest when compiling '{s}': {s}", .{ win32_resource.src.src_path, @errorName(err) }); + }; + } + + const res_basename = try std.fmt.allocPrint(arena, "{s}.res", .{rc_basename_noext}); + + win32_resource.status = .{ + .success = .{ + .res_path = try comp.local_cache_directory.join(comp.gpa, &[_][]const u8{ + "o", &digest, res_basename, + }), + .lock = man.toOwnedLock(), + }, + }; +} + pub fn tmpFilePath(comp: *Compilation, ally: Allocator, suffix: []const u8) error{OutOfMemory}![]const u8 { const s = std.fs.path.sep_str; const rand_int = std.crypto.random.int(u64); @@ -4350,7 +4839,7 @@ pub fn addCCArgs( try argv.appendSlice(&[_][]const u8{ "-target", llvm_triple }); switch (ext) { - .c, .cpp, .m, .mm, .h, .cu => { + .c, .cpp, .m, .mm, .h, .cu, .rc => { try argv.appendSlice(&[_][]const u8{ "-nostdinc", "-fno-spell-checking", @@ -4378,9 +4867,16 @@ pub fn addCCArgs( try argv.append("-isystem"); try argv.append(c_headers_dir); - for (comp.libc_include_dir_list) |include_dir| { - try argv.append("-isystem"); - try argv.append(include_dir); + if (ext == .rc) { + for (comp.rc_include_dir_list) |include_dir| { + try argv.append("-isystem"); + try argv.append(include_dir); + } + } else { + for (comp.libc_include_dir_list) |include_dir| { + try argv.append("-isystem"); + try argv.append(include_dir); + } } if (target.cpu.model.llvm_name) |llvm_name| { @@ -4692,6 +5188,253 @@ fn failCObjWithOwnedErrorMsg( return error.AnalysisFail; } +/// The include directories used when preprocessing .rc files are separate from the +/// target. Which include directories are used is determined by `options.rc_includes`. +/// +/// Note: It should be okay that the include directories used when compiling .rc +/// files differ from the include directories used when compiling the main +/// binary, since the .res format is not dependent on anything ABI-related. The +/// only relevant differences would be things like `#define` constants being +/// different in the MinGW headers vs the MSVC headers, but any such +/// differences would likely be a MinGW bug. +fn detectWin32ResourceIncludeDirs(arena: Allocator, options: InitOptions) !LibCDirs { + // Set the includes to .none here when there are no rc files to compile + var includes = if (options.rc_source_files.len > 0) options.rc_includes else .none; + if (builtin.target.os.tag != .windows) { + switch (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 => includes = .gnu, + .none, .gnu => {}, + } + } + while (true) { + switch (includes) { + .any, .msvc => return detectLibCIncludeDirs( + arena, + options.zig_lib_directory.path.?, + .{ + .cpu = options.target.cpu, + .os = options.target.os, + .abi = .msvc, + .ofmt = options.target.ofmt, + }, + options.is_native_abi, + // The .rc preprocessor will need to know the libc include dirs even if we + // are not linking libc, so force 'link_libc' to true + true, + options.libc_installation, + ) catch |err| { + if (includes == .any) { + // fall back to mingw + includes = .gnu; + continue; + } + return err; + }, + .gnu => return detectLibCFromBuilding(arena, options.zig_lib_directory.path.?, .{ + .cpu = options.target.cpu, + .os = options.target.os, + .abi = .gnu, + .ofmt = options.target.ofmt, + }), + .none => return LibCDirs{ + .libc_include_dir_list = &[0][]u8{}, + .libc_installation = null, + .libc_framework_dir_list = &.{}, + .sysroot = null, + }, + } + } +} + +fn failWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, comptime format: []const u8, args: anytype) SemaError { + @setCold(true); + var bundle: ErrorBundle.Wip = undefined; + try bundle.init(comp.gpa); + errdefer bundle.deinit(); + try bundle.addRootErrorMessage(.{ + .msg = try bundle.printString(format, args), + .src_loc = try bundle.addSourceLocation(.{ + .src_path = try bundle.addString(win32_resource.src.src_path), + .line = 0, + .column = 0, + .span_start = 0, + .span_main = 0, + .span_end = 0, + }), + }); + const finished_bundle = try bundle.toOwnedBundle(""); + return comp.failWin32ResourceWithOwnedBundle(win32_resource, finished_bundle); +} + +fn failWin32ResourceWithOwnedBundle( + comp: *Compilation, + win32_resource: *Win32Resource, + err_bundle: ErrorBundle, +) SemaError { + @setCold(true); + { + comp.mutex.lock(); + defer comp.mutex.unlock(); + try comp.failed_win32_resources.putNoClobber(comp.gpa, win32_resource, err_bundle); + } + win32_resource.status = .failure; + return error.AnalysisFail; +} + +fn failWin32ResourceCli( + comp: *Compilation, + win32_resource: *Win32Resource, + diagnostics: *resinator.cli.Diagnostics, +) SemaError { + @setCold(true); + + var bundle: ErrorBundle.Wip = undefined; + try bundle.init(comp.gpa); + errdefer bundle.deinit(); + + try bundle.addRootErrorMessage(.{ + .msg = try bundle.addString("invalid command line option(s)"), + .src_loc = try bundle.addSourceLocation(.{ + .src_path = try bundle.addString(win32_resource.src.src_path), + .line = 0, + .column = 0, + .span_start = 0, + .span_main = 0, + .span_end = 0, + }), + }); + + var cur_err: ?ErrorBundle.ErrorMessage = null; + var cur_notes: std.ArrayListUnmanaged(ErrorBundle.ErrorMessage) = .{}; + defer cur_notes.deinit(comp.gpa); + for (diagnostics.errors.items) |err_details| { + switch (err_details.type) { + .err => { + if (cur_err) |err| { + try win32ResourceFlushErrorMessage(&bundle, err, cur_notes.items); + } + cur_err = .{ + .msg = try bundle.addString(err_details.msg.items), + }; + cur_notes.clearRetainingCapacity(); + }, + .warning => cur_err = null, + .note => { + if (cur_err == null) continue; + cur_err.?.notes_len += 1; + try cur_notes.append(comp.gpa, .{ + .msg = try bundle.addString(err_details.msg.items), + }); + }, + } + } + if (cur_err) |err| { + try win32ResourceFlushErrorMessage(&bundle, err, cur_notes.items); + } + + const finished_bundle = try bundle.toOwnedBundle(""); + return comp.failWin32ResourceWithOwnedBundle(win32_resource, finished_bundle); +} + +fn failWin32ResourceCompile( + comp: *Compilation, + win32_resource: *Win32Resource, + source: []const u8, + diagnostics: *resinator.errors.Diagnostics, + mappings: resinator.source_mapping.SourceMappings, +) SemaError { + @setCold(true); + + var bundle: ErrorBundle.Wip = undefined; + try bundle.init(comp.gpa); + errdefer bundle.deinit(); + + var msg_buf: std.ArrayListUnmanaged(u8) = .{}; + defer msg_buf.deinit(comp.gpa); + var cur_err: ?ErrorBundle.ErrorMessage = null; + var cur_notes: std.ArrayListUnmanaged(ErrorBundle.ErrorMessage) = .{}; + defer cur_notes.deinit(comp.gpa); + for (diagnostics.errors.items) |err_details| { + switch (err_details.type) { + .hint => continue, + // Clear the current error so that notes don't bleed into unassociated errors + .warning => { + cur_err = null; + continue; + }, + .note => if (cur_err == null) continue, + .err => {}, + } + const corresponding_span = mappings.get(err_details.token.line_number); + const corresponding_file = mappings.files.get(corresponding_span.filename_offset); + + const source_line_start = err_details.token.getLineStart(source); + const column = err_details.token.calculateColumn(source, 1, source_line_start); + const err_line = corresponding_span.start_line; + + msg_buf.clearRetainingCapacity(); + try err_details.render(msg_buf.writer(comp.gpa), source, diagnostics.strings.items); + + const src_loc = src_loc: { + var src_loc: ErrorBundle.SourceLocation = .{ + .src_path = try bundle.addString(corresponding_file), + .line = @intCast(err_line - 1), // 1-based -> 0-based + .column = @intCast(column), + .span_start = 0, + .span_main = 0, + .span_end = 0, + }; + if (err_details.print_source_line) { + const source_line = err_details.token.getLine(source, source_line_start); + const visual_info = err_details.visualTokenInfo(source_line_start, source_line_start + source_line.len); + src_loc.span_start = @intCast(visual_info.point_offset - visual_info.before_len); + src_loc.span_main = @intCast(visual_info.point_offset); + src_loc.span_end = @intCast(visual_info.point_offset + 1 + visual_info.after_len); + src_loc.source_line = try bundle.addString(source_line); + } + break :src_loc try bundle.addSourceLocation(src_loc); + }; + + switch (err_details.type) { + .err => { + if (cur_err) |err| { + try win32ResourceFlushErrorMessage(&bundle, err, cur_notes.items); + } + cur_err = .{ + .msg = try bundle.addString(msg_buf.items), + .src_loc = src_loc, + }; + cur_notes.clearRetainingCapacity(); + }, + .note => { + cur_err.?.notes_len += 1; + try cur_notes.append(comp.gpa, .{ + .msg = try bundle.addString(msg_buf.items), + .src_loc = src_loc, + }); + }, + .warning, .hint => unreachable, + } + } + if (cur_err) |err| { + try win32ResourceFlushErrorMessage(&bundle, err, cur_notes.items); + } + + const finished_bundle = try bundle.toOwnedBundle(""); + return comp.failWin32ResourceWithOwnedBundle(win32_resource, finished_bundle); +} + +fn win32ResourceFlushErrorMessage(wip: *ErrorBundle.Wip, msg: ErrorBundle.ErrorMessage, notes: []const ErrorBundle.ErrorMessage) !void { + try wip.addRootErrorMessage(msg); + const notes_start = try wip.reserveNotes(@intCast(notes.len)); + for (notes_start.., notes) |i, note| { + wip.extra.items[i] = @intFromEnum(wip.addErrorMessageAssumeCapacity(note)); + } +} + pub const FileExt = enum { c, cpp, @@ -4708,6 +5451,7 @@ pub const FileExt = enum { static_library, zig, def, + rc, res, unknown, @@ -4724,6 +5468,7 @@ pub const FileExt = enum { .static_library, .zig, .def, + .rc, .res, .unknown, => false, @@ -4747,6 +5492,7 @@ pub const FileExt = enum { .static_library => target.staticLibSuffix(), .zig => ".zig", .def => ".def", + .rc => ".rc", .res => ".res", .unknown => "", }; @@ -4839,7 +5585,9 @@ pub fn classifyFileExt(filename: []const u8) FileExt { return .cu; } else if (mem.endsWith(u8, filename, ".def")) { return .def; - } else if (mem.endsWith(u8, filename, ".res")) { + } else if (std.ascii.endsWithIgnoreCase(filename, ".rc")) { + return .rc; + } else if (std.ascii.endsWithIgnoreCase(filename, ".res")) { return .res; } else { return .unknown; @@ -4983,6 +5731,13 @@ fn detectLibCFromLibCInstallation(arena: Allocator, target: Target, lci: *const if (!is_redundant) list.appendAssumeCapacity(lci.sys_include_dir.?); if (target.os.tag == .windows) { + if (std.fs.path.dirname(lci.sys_include_dir.?)) |sys_include_dir_parent| { + // This include path will only exist when the optional "Desktop development with C++" + // is installed. It contains headers, .rc files, and resources. It is especially + // necessary when working with Windows resources. + const atlmfc_dir = try std.fs.path.join(arena, &[_][]const u8{ sys_include_dir_parent, "atlmfc", "include" }); + list.appendAssumeCapacity(atlmfc_dir); + } if (std.fs.path.dirname(lci.include_dir.?)) |include_dir_parent| { const um_dir = try std.fs.path.join(arena, &[_][]const u8{ include_dir_parent, "um" }); list.appendAssumeCapacity(um_dir); |
