diff --git a/lib/compiler/build_runner.zig b/lib/compiler/build_runner.zig index 421d2e72f1..eb754c4e10 100644 --- a/lib/compiler/build_runner.zig +++ b/lib/compiler/build_runner.zig @@ -339,6 +339,10 @@ pub fn main() !void { try builder.runBuild(root); } + for (builder.top_level_steps.values()) |s| { + try finalizeSteps(&s.step); + } + if (graph.needed_lazy_dependencies.entries.len != 0) { var buffer: std.ArrayListUnmanaged(u8) = .empty; for (graph.needed_lazy_dependencies.keys()) |k| { @@ -636,6 +640,7 @@ fn runStepNames( test_count += s.test_results.test_count; switch (s.state) { + .unfinalized => unreachable, .precheck_unstarted => unreachable, .precheck_started => unreachable, .running => unreachable, @@ -781,6 +786,7 @@ fn printStepStatus( run: *const Run, ) !void { switch (s.state) { + .unfinalized => unreachable, .precheck_unstarted => unreachable, .precheck_started => unreachable, .precheck_done => unreachable, @@ -978,6 +984,34 @@ fn printTreeStep( } } +/// Traverse the dependency graph after the user build() call, +/// this allows for checks and postprocessing after the steps are fully configured by the user. +fn finalizeSteps( + s: *Step, +) !void { + switch (s.state) { + .unfinalized => { + try s.finalize(); + s.state = .precheck_unstarted; + + for (s.dependencies.items) |dep| { + try finalizeSteps(dep); + } + }, + + .precheck_unstarted => {}, + + .precheck_started => unreachable, + .precheck_done => unreachable, + .dependency_failure => unreachable, + .running => unreachable, + .success => unreachable, + .failure => unreachable, + .skipped => unreachable, + .skipped_oom => unreachable, + } +} + /// Traverse the dependency graph depth-first and make it undirected by having /// steps know their dependants (they only know dependencies at start). /// Along the way, check that there is no dependency loop, and record the steps @@ -996,6 +1030,7 @@ fn constructGraphAndCheckForDependencyLoop( rand: std.Random, ) !void { switch (s.state) { + .unfinalized => unreachable, .precheck_started => { std.debug.print("dependency loop detected:\n {s}\n", .{s.name}); return error.DependencyLoopDetected; @@ -1058,6 +1093,7 @@ fn workerMakeOneStep( // dependency is not finished yet. return; }, + .unfinalized => unreachable, .precheck_unstarted => unreachable, .precheck_started => unreachable, } diff --git a/lib/std/Build.zig b/lib/std/Build.zig index 041de3e06f..c39d3cec90 100644 --- a/lib/std/Build.zig +++ b/lib/std/Build.zig @@ -796,6 +796,31 @@ pub fn addObject(b: *Build, options: ObjectOptions) *Step.Compile { }); } +pub const PchOptions = struct { + name: []const u8, + target: ResolvedTarget, + optimize: std.builtin.OptimizeMode, + max_rss: usize = 0, + link_libc: ?bool = null, + link_libcpp: ?bool = null, +}; + +pub fn addPrecompiledCHeader(b: *Build, options: PchOptions, source: Module.CSourceFile) *Step.Compile { + const pch = Step.Compile.create(b, .{ + .name = options.name, + .root_module = .{ + .target = options.target, + .optimize = options.optimize, + .link_libc = options.link_libc, + .link_libcpp = options.link_libcpp, + }, + .kind = .pch, + .max_rss = options.max_rss, + }); + pch.addCSourceFile(source); + return pch; +} + pub const SharedLibraryOptions = struct { name: []const u8, /// To choose the same computer as the one building the package, pass the diff --git a/lib/std/Build/Module.zig b/lib/std/Build/Module.zig index 347695a4c0..0e221a88fc 100644 --- a/lib/std/Build/Module.zig +++ b/lib/std/Build/Module.zig @@ -46,11 +46,36 @@ pub const RPath = union(enum) { special: []const u8, }; +// subset of Compilation.FileExt +pub const AsmSourceLang = enum { + assembly, + assembly_with_cpp, + + pub fn getLangName(lang: @This()) []const u8 { + return switch (lang) { + .assembly => "assembler", + .assembly_with_cpp => "assembler-with-cpp", + }; + } +}; + +pub const AsmSourceFile = struct { + file: LazyPath, + lang: ?AsmSourceLang = null, + + pub fn dupe(file: AsmSourceFile, b: *std.Build) AsmSourceFile { + return .{ + .file = file.file.dupe(b), + .lang = file.lang, + }; + } +}; + pub const LinkObject = union(enum) { static_path: LazyPath, other_step: *Step.Compile, system_lib: SystemLib, - assembly_file: LazyPath, + assembly_file: *AsmSourceFile, c_source_file: *CSourceFile, c_source_files: *CSourceFiles, win32_resource_file: *RcSourceFile, @@ -78,22 +103,87 @@ pub const SystemLib = struct { pub const SearchStrategy = enum { paths_first, mode_first, no_fallback }; }; +/// Supported languages for "zig clang -x ". +// subset of Compilation.FileExt +pub const CSourceLang = enum { + /// "c" + c, + /// "c-header" + h, + /// "c++" + cpp, + /// "c++-header" + hpp, + /// "objective-c" + m, + /// "objective-c-header" + hm, + /// "objective-c++" + mm, + /// "objective-c++-header" + hmm, + /// "assembler" + assembly, + /// "assembler-with-cpp" + assembly_with_cpp, + /// "cuda" + cu, + + pub fn getLangName(lang: @This()) []const u8 { + return switch (lang) { + .assembly => "assembler", + .assembly_with_cpp => "assembler-with-cpp", + .c => "c", + .cpp => "c++", + .h => "c-header", + .hpp => "c++-header", + .hm => "objective-c-header", + .hmm => "objective-c++-header", + .cu => "cuda", + .m => "objective-c", + .mm => "objective-c++", + }; + } +}; + +pub const PrecompiledHeader = union(enum) { + /// automatically create the PCH compile step for the source header file, + /// inheriting the options from the parent compile step. + source_header: struct { path: LazyPath, lang: ?CSourceLang = null }, + + /// final PCH compile step, + /// can be provided by the user or else will be created from the `source_header` field during step finalization. + pch_step: *Step.Compile, + + pub fn getPath(pch: PrecompiledHeader, b: *std.Build) []const u8 { + switch (pch) { + .source_header => unreachable, + .pch_step => |pch_step| return pch_step.getEmittedBin().getPath(b), + } + } +}; pub const CSourceFiles = struct { root: LazyPath, /// `files` is relative to `root`, which is /// the build root by default files: []const []const u8, + lang: ?CSourceLang = null, flags: []const []const u8, + precompiled_header: ?PrecompiledHeader = null, }; pub const CSourceFile = struct { file: LazyPath, + lang: ?CSourceLang = null, flags: []const []const u8 = &.{}, + precompiled_header: ?PrecompiledHeader = null, pub fn dupe(file: CSourceFile, b: *std.Build) CSourceFile { return .{ .file = file.file.dupe(b), + .lang = file.lang, .flags = b.dupeStrings(file.flags), + .precompiled_header = file.precompiled_header, }; } }; @@ -324,10 +414,9 @@ fn addShallowDependencies(m: *Module, dependee: *Module) void { addLazyPathDependenciesOnly(m, compile.getEmittedIncludeTree()); }, - .static_path, - .assembly_file, - => |lp| addLazyPathDependencies(m, dependee, lp), + .static_path => |lp| addLazyPathDependencies(m, dependee, lp), + .assembly_file => |x| addLazyPathDependencies(m, dependee, x.file), .c_source_file => |x| addLazyPathDependencies(m, dependee, x.file), .win32_resource_file => |x| addLazyPathDependencies(m, dependee, x.file), @@ -361,7 +450,7 @@ fn addStepDependencies(m: *Module, module: *Module, dependee: *Step) void { } } -fn addStepDependenciesOnly(m: *Module, dependee: *Step) void { +pub fn addStepDependenciesOnly(m: *Module, dependee: *Step) void { for (m.depending_steps.keys()) |compile| { compile.step.dependOn(dependee); } @@ -523,7 +612,9 @@ pub const AddCSourceFilesOptions = struct { /// package that owns the `Compile` step. root: ?LazyPath = null, files: []const []const u8, + lang: ?CSourceLang = null, flags: []const []const u8 = &.{}, + precompiled_header: ?PrecompiledHeader = null, }; /// Handy when you have many C/C++ source files and want them all to have the same flags. @@ -544,10 +635,22 @@ pub fn addCSourceFiles(m: *Module, options: AddCSourceFilesOptions) void { c_source_files.* = .{ .root = options.root orelse b.path(""), .files = b.dupeStrings(options.files), + .lang = options.lang, .flags = b.dupeStrings(options.flags), + .precompiled_header = options.precompiled_header, }; m.link_objects.append(allocator, .{ .c_source_files = c_source_files }) catch @panic("OOM"); addLazyPathDependenciesOnly(m, c_source_files.root); + + if (options.precompiled_header) |pch| { + switch (pch) { + .source_header => |src| addLazyPathDependenciesOnly(m, src.path), + .pch_step => |step| { + _ = step.getEmittedBin(); // Indicate there is a dependency on the outputted binary. + addStepDependenciesOnly(m, &step.step); + }, + } + } } pub fn addCSourceFile(m: *Module, source: CSourceFile) void { @@ -557,6 +660,16 @@ pub fn addCSourceFile(m: *Module, source: CSourceFile) void { c_source_file.* = source.dupe(b); m.link_objects.append(allocator, .{ .c_source_file = c_source_file }) catch @panic("OOM"); addLazyPathDependenciesOnly(m, source.file); + + if (source.precompiled_header) |pch| { + switch (pch) { + .source_header => |src| addLazyPathDependenciesOnly(m, src.path), + .pch_step => |step| { + _ = step.getEmittedBin(); // Indicate there is a dependency on the outputted binary. + addStepDependenciesOnly(m, &step.step); + }, + } + } } /// Resource files must have the extension `.rc`. @@ -579,10 +692,12 @@ pub fn addWin32ResourceFile(m: *Module, source: RcSourceFile) void { } } -pub fn addAssemblyFile(m: *Module, source: LazyPath) void { +pub fn addAssemblyFile(m: *Module, source: AsmSourceFile) void { const b = m.owner; - m.link_objects.append(b.allocator, .{ .assembly_file = source.dupe(b) }) catch @panic("OOM"); - addLazyPathDependenciesOnly(m, source); + const source_file = b.allocator.create(AsmSourceFile) catch @panic("OOM"); + source_file.* = source.dupe(b); + m.link_objects.append(b.allocator, .{ .assembly_file = source_file }) catch @panic("OOM"); + addLazyPathDependenciesOnly(m, source.file); } pub fn addObjectFile(m: *Module, object: LazyPath) void { diff --git a/lib/std/Build/Step.zig b/lib/std/Build/Step.zig index 421c8fa579..797361566c 100644 --- a/lib/std/Build/Step.zig +++ b/lib/std/Build/Step.zig @@ -1,6 +1,7 @@ id: Id, name: []const u8, owner: *Build, +finalizeFn: FinalizeFn, makeFn: MakeFn, dependencies: std.ArrayList(*Step), @@ -68,6 +69,8 @@ pub const TestResults = struct { } }; +pub const FinalizeFn = *const fn (step: *Step) anyerror!void; + pub const MakeOptions = struct { progress_node: std.Progress.Node, thread_pool: *std.Thread.Pool, @@ -77,6 +80,7 @@ pub const MakeOptions = struct { pub const MakeFn = *const fn (step: *Step, options: MakeOptions) anyerror!void; pub const State = enum { + unfinalized, precheck_unstarted, precheck_started, /// This is also used to indicate "dirty" steps that have been modified @@ -183,6 +187,7 @@ pub const StepOptions = struct { id: Id, name: []const u8, owner: *Build, + finalizeFn: FinalizeFn = finalizeNoOp, makeFn: MakeFn = makeNoOp, first_ret_addr: ?usize = null, max_rss: usize = 0, @@ -195,11 +200,12 @@ pub fn init(options: StepOptions) Step { .id = options.id, .name = arena.dupe(u8, options.name) catch @panic("OOM"), .owner = options.owner, + .finalizeFn = options.finalizeFn, .makeFn = options.makeFn, .dependencies = std.ArrayList(*Step).init(arena), .dependants = .{}, .inputs = Inputs.init, - .state = .precheck_unstarted, + .state = .unfinalized, .max_rss = options.max_rss, .debug_stack_trace = blk: { const addresses = arena.alloc(usize, options.owner.debug_stack_frames_count) catch @panic("OOM"); @@ -222,6 +228,11 @@ pub fn init(options: StepOptions) Step { }; } +pub fn finalize(s: *Step) !void { + assert(s.state == .unfinalized); + try s.finalizeFn(s); +} + /// If the Step's `make` function reports `error.MakeFailed`, it indicates they /// have already reported the error. Otherwise, we add a simple error report /// here. @@ -266,6 +277,10 @@ pub fn getStackTrace(s: *Step) ?std.builtin.StackTrace { }; } +fn finalizeNoOp(step: *Step) anyerror!void { + _ = step; +} + fn makeNoOp(step: *Step, options: MakeOptions) anyerror!void { _ = options; diff --git a/lib/std/Build/Step/Compile.zig b/lib/std/Build/Step/Compile.zig index f0dad8b49a..0d703e5dd6 100644 --- a/lib/std/Build/Step/Compile.zig +++ b/lib/std/Build/Step/Compile.zig @@ -216,9 +216,9 @@ generated_h: ?*GeneratedFile, /// Defaults to `std.math.maxInt(u16)` error_limit: ?u32 = null, -/// Computed during make(). +/// Computed during finalize(). is_linking_libc: bool = false, -/// Computed during make(). +/// Computed during finalize(). is_linking_libcpp: bool = false, no_builtin: bool = false, @@ -284,6 +284,7 @@ pub const Kind = enum { exe, lib, obj, + pch, @"test", }; @@ -367,6 +368,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile { .exe => "zig build-exe", .lib => "zig build-lib", .obj => "zig build-obj", + .pch => "zig build-pch", .@"test" => "zig test", }, name_adjusted, @@ -374,17 +376,21 @@ pub fn create(owner: *std.Build, options: Options) *Compile { resolved_target.query.zigTriple(owner.allocator) catch @panic("OOM"), }); - const out_filename = std.zig.binNameAlloc(owner.allocator, .{ - .root_name = name, - .target = target, - .output_mode = switch (options.kind) { - .lib => .Lib, - .obj => .Obj, - .exe, .@"test" => .Exe, - }, - .link_mode = options.linkage, - .version = options.version, - }) catch @panic("OOM"); + const out_filename = if (options.kind == .pch) + std.fmt.allocPrint(owner.allocator, "{s}.pch", .{name}) catch @panic("OOM") + else + std.zig.binNameAlloc(owner.allocator, .{ + .root_name = name, + .target = target, + .output_mode = switch (options.kind) { + .lib => .Lib, + .obj => .Obj, + .exe, .@"test" => .Exe, + .pch => unreachable, + }, + .link_mode = options.linkage, + .version = options.version, + }) catch @panic("OOM"); const compile = owner.allocator.create(Compile) catch @panic("OOM"); compile.* = .{ @@ -398,6 +404,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile { .id = base_id, .name = step_name, .owner = owner, + .finalizeFn = finalize, .makeFn = make, .max_rss = options.max_rss, }), @@ -815,10 +822,12 @@ pub fn linkFrameworkWeak(c: *Compile, name: []const u8) void { /// Handy when you have many C/C++ source files and want them all to have the same flags. pub fn addCSourceFiles(compile: *Compile, options: Module.AddCSourceFilesOptions) void { + assert(compile.kind != .pch); // pch can only be generated from a single C header file compile.root_module.addCSourceFiles(options); } pub fn addCSourceFile(compile: *Compile, source: Module.CSourceFile) void { + assert(compile.kind != .pch or compile.root_module.link_objects.items.len == 0); // pch can only be generated from a single C header file compile.root_module.addCSourceFile(source); } @@ -826,6 +835,7 @@ pub fn addCSourceFile(compile: *Compile, source: Module.CSourceFile) void { /// Can be called regardless of target. The .rc file will be ignored /// if the target object format does not support embedded resources. pub fn addWin32ResourceFile(compile: *Compile, source: Module.RcSourceFile) void { + assert(compile.kind != .pch); // pch can only be generated from a single C header file compile.root_module.addWin32ResourceFile(source); } @@ -905,15 +915,18 @@ pub fn getEmittedLlvmBc(compile: *Compile) LazyPath { return compile.getEmittedFileGeneric(&compile.generated_llvm_bc); } -pub fn addAssemblyFile(compile: *Compile, source: LazyPath) void { +pub fn addAssemblyFile(compile: *Compile, source: Module.AsmSourceFile) void { + assert(compile.kind != .pch); // pch can only be generated from a single C header file compile.root_module.addAssemblyFile(source); } pub fn addObjectFile(compile: *Compile, source: LazyPath) void { + assert(compile.kind != .pch); // pch can only be generated from a single C header file compile.root_module.addObjectFile(source); } pub fn addObject(compile: *Compile, object: *Compile) void { + assert(compile.kind != .pch); // pch can only be generated from a single C header file compile.root_module.addObject(object); } @@ -1038,6 +1051,7 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 { .lib => "build-lib", .exe => "build-exe", .obj => "build-obj", + .pch => "build-pch", .@"test" => "test", }; try zig_args.append(cmd); @@ -1089,22 +1103,13 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 { var prev_has_cflags = false; var prev_has_rcflags = false; + var prev_has_xflag = false; var prev_search_strategy: Module.SystemLib.SearchStrategy = .paths_first; var prev_preferred_link_mode: std.builtin.LinkMode = .dynamic; // Track the number of positional arguments so that a nice error can be // emitted if there is nothing to link. var total_linker_objects: usize = @intFromBool(compile.root_module.root_source_file != null); - { - // Fully recursive iteration including dynamic libraries to detect - // libc and libc++ linkage. - var dep_it = compile.root_module.iterateDependencies(compile, true); - while (dep_it.next()) |key| { - if (key.module.link_libc == true) compile.is_linking_libc = true; - if (key.module.link_libcpp == true) compile.is_linking_libcpp = true; - } - } - var cli_named_modules = try CliNamedModules.init(arena, &compile.root_module); // For this loop, don't chase dynamic libraries because their link @@ -1127,6 +1132,12 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 { // Inherit dependencies on system libraries and static libraries. for (dep.module.link_objects.items) |link_object| { + if (prev_has_xflag and link_object != .c_source_file and link_object != .c_source_files and link_object != .assembly_file) { + try zig_args.append("-x"); + try zig_args.append("none"); + prev_has_xflag = false; + } + switch (link_object) { .static_path => |static_path| { if (my_responsibility) { @@ -1210,6 +1221,7 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 { switch (other.kind) { .exe => return step.fail("cannot link with an executable build artifact", .{}), .@"test" => return step.fail("cannot link with a test", .{}), + .pch => @panic("Cannot link with a precompiled header file"), .obj => { const included_in_lib_or_obj = !my_responsibility and (dep.compile.?.kind == .lib or dep.compile.?.kind == .obj); @@ -1257,14 +1269,25 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 { try zig_args.append("--"); prev_has_cflags = false; } - try zig_args.append(asm_file.getPath2(dep.module.owner, step)); + + if (asm_file.lang) |lang| { + try zig_args.append("-x"); + try zig_args.append(lang.getLangName()); + prev_has_xflag = true; + } else if (prev_has_xflag) { + try zig_args.append("-x"); + try zig_args.append("none"); + prev_has_xflag = false; + } + + try zig_args.append(asm_file.file.getPath2(dep.module.owner, step)); total_linker_objects += 1; }, .c_source_file => |c_source_file| l: { if (!my_responsibility) break :l; - if (c_source_file.flags.len == 0) { + if (c_source_file.flags.len == 0 and c_source_file.precompiled_header == null) { if (prev_has_cflags) { try zig_args.append("-cflags"); try zig_args.append("--"); @@ -1275,9 +1298,25 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 { for (c_source_file.flags) |arg| { try zig_args.append(arg); } + if (c_source_file.precompiled_header) |pch| { + try zig_args.append("-include-pch"); + try zig_args.append(pch.getPath(b)); + try zig_args.append("-fpch-validate-input-files-content"); + } try zig_args.append("--"); prev_has_cflags = true; } + + if (c_source_file.lang) |lang| { + try zig_args.append("-x"); + try zig_args.append(lang.getLangName()); + prev_has_xflag = true; + } else if (prev_has_xflag) { + try zig_args.append("-x"); + try zig_args.append("none"); + prev_has_xflag = false; + } + try zig_args.append(c_source_file.file.getPath2(dep.module.owner, step)); total_linker_objects += 1; }, @@ -1285,7 +1324,7 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 { .c_source_files => |c_source_files| l: { if (!my_responsibility) break :l; - if (c_source_files.flags.len == 0) { + if (c_source_files.flags.len == 0 and c_source_files.precompiled_header == null) { if (prev_has_cflags) { try zig_args.append("-cflags"); try zig_args.append("--"); @@ -1296,10 +1335,27 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 { for (c_source_files.flags) |flag| { try zig_args.append(flag); } + + if (c_source_files.precompiled_header) |pch| { + try zig_args.append("-include-pch"); + try zig_args.append(pch.getPath(b)); + try zig_args.append("-fpch-validate-input-files-content"); + } + try zig_args.append("--"); prev_has_cflags = true; } + if (c_source_files.lang) |lang| { + try zig_args.append("-x"); + try zig_args.append(lang.getLangName()); + prev_has_xflag = true; + } else if (prev_has_xflag) { + try zig_args.append("-x"); + try zig_args.append("none"); + prev_has_xflag = false; + } + const root_path = c_source_files.root.getPath2(dep.module.owner, step); for (c_source_files.files) |file| { try zig_args.append(b.pathJoin(&.{ root_path, file })); @@ -1780,6 +1836,90 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 { return try zig_args.toOwnedSlice(); } +fn finalize(step: *Step) !void { + const compile: *Compile = @fieldParentPtr("step", step); + const b = step.owner; + + { + // Fully recursive iteration including dynamic libraries to detect + // libc and libc++ linkage. + var dep_it = compile.root_module.iterateDependencies(compile, true); + while (dep_it.next()) |key| { + if (key.module.link_libc == true) compile.is_linking_libc = true; + if (key.module.link_libcpp == true) compile.is_linking_libcpp = true; + } + } + + if (compile.kind == .pch) { + // precompiled headers must have a single input header file. + var it = compile.root_module.iterateDependencies(compile, false); + const link_objects = it.next().?.module.link_objects; + assert(link_objects.items.len == 1 and link_objects.items[0] == .c_source_file); + assert(it.next() == null); + } + + // add additional compile steps for precompiled headers + for (compile.root_module.link_objects.items) |*link_object| { + const pch_ptr: *Module.PrecompiledHeader, const flags: []const []const u8 = blk: { + switch (link_object.*) { + .c_source_file => |src| { + if (src.precompiled_header) |*pch| break :blk .{ pch, src.flags }; + }, + .c_source_files => |src| { + if (src.precompiled_header) |*pch| break :blk .{ pch, src.flags }; + }, + else => {}, + } + + continue; + }; + + switch (pch_ptr.*) { + .pch_step => { + // step customized by the user, nothing to do. + }, + .source_header => |src| { + const name = switch (src.path) { + .src_path => |sp| fs.path.basename(sp.sub_path), + .cwd_relative => |p| fs.path.basename(p), + .generated => "generated", + .dependency => "dependency", + }; + + const step_name = b.fmt("zig build-pch {s}{s} {s}", .{ + name, + @tagName(compile.root_module.optimize orelse .Debug), + compile.root_module.resolved_target.?.query.zigTriple(b.allocator) catch @panic("OOM"), + }); + + // We generate a new compile step for each use, + // leveraging the cache system to reuse the generated pch file when possible. + const compile_pch = b.allocator.create(Compile) catch @panic("OOM"); + + // For robustness, suppose all options have an impact on the header compilation. + // (instead of auditing each llvm version for flags observable from header compilation) + // So, copy everything and minimally adjust as needed: + compile_pch.* = compile.*; + + compile_pch.kind = .pch; + compile_pch.step.name = step_name; + compile_pch.name = name; + compile_pch.out_filename = std.fmt.allocPrint(b.allocator, "{s}.pch", .{name}) catch @panic("OOM"); + compile_pch.installed_headers = .init(b.allocator); + compile_pch.force_undefined_symbols = .init(b.allocator); + + compile_pch.root_module.link_objects = .{}; + compile_pch.addCSourceFile(.{ .file = src.path, .lang = src.lang, .flags = flags }); + + // finalize the parent compile step by modifying it to use the generated pch compile step + pch_ptr.* = .{ .pch_step = compile_pch }; + _ = compile_pch.getEmittedBin(); // Indicate there is a dependency on the outputted binary. + compile.root_module.addStepDependenciesOnly(&compile_pch.step); + }, + } + } +} + fn make(step: *Step, options: Step.MakeOptions) !void { const b = step.owner; const compile: *Compile = @fieldParentPtr("step", step); diff --git a/lib/std/Build/Step/InstallArtifact.zig b/lib/std/Build/Step/InstallArtifact.zig index 3d404eb8ca..7291c22c2b 100644 --- a/lib/std/Build/Step/InstallArtifact.zig +++ b/lib/std/Build/Step/InstallArtifact.zig @@ -56,7 +56,7 @@ pub fn create(owner: *std.Build, artifact: *Step.Compile, options: Options) *Ins const dest_dir: ?InstallDir = switch (options.dest_dir) { .disabled => null, .default => switch (artifact.kind) { - .obj => @panic("object files have no standard installation procedure"), + .obj, .pch => @panic("object files have no standard installation procedure"), .exe, .@"test" => .bin, .lib => if (artifact.isDll()) .bin else .lib, }, diff --git a/src/Compilation.zig b/src/Compilation.zig index a228d61257..8111273cae 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -4691,11 +4691,11 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: std.Pr else "/dev/null"; - try argv.ensureUnusedCapacity(6); + try argv.ensureUnusedCapacity(7); switch (comp.clang_preprocessor_mode) { .no => argv.appendSliceAssumeCapacity(&.{ "-c", "-o", out_obj_path }), .yes => argv.appendSliceAssumeCapacity(&.{ "-E", "-o", out_obj_path }), - .pch => argv.appendSliceAssumeCapacity(&.{ "-Xclang", "-emit-pch", "-o", out_obj_path }), + .pch => argv.appendSliceAssumeCapacity(&.{ "-Xclang", "-emit-pch", "-fpch-validate-input-files-content", "-o", out_obj_path }), .stdout => argv.appendAssumeCapacity("-E"), } @@ -4734,11 +4734,11 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: std.Pr try argv.appendSlice(c_object.src.extra_flags); try argv.appendSlice(c_object.src.cache_exempt_flags); - try argv.ensureUnusedCapacity(6); + try argv.ensureUnusedCapacity(7); switch (comp.clang_preprocessor_mode) { .no => argv.appendSliceAssumeCapacity(&.{ "-c", "-o", out_obj_path }), .yes => argv.appendSliceAssumeCapacity(&.{ "-E", "-o", out_obj_path }), - .pch => argv.appendSliceAssumeCapacity(&.{ "-Xclang", "-emit-pch", "-o", out_obj_path }), + .pch => argv.appendSliceAssumeCapacity(&.{ "-Xclang", "-emit-pch", "-fpch-validate-input-files-content", "-o", out_obj_path }), .stdout => argv.appendAssumeCapacity("-E"), } if (out_diag_path) |diag_file_path| { diff --git a/src/main.zig b/src/main.zig index 291820cb75..da974f253b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -82,6 +82,7 @@ const normal_usage = \\ build-exe Create executable from source or object files \\ build-lib Create library from source or object files \\ build-obj Create object from source or object files + \\ build-pch Create a precompiled header from a c or c++ header \\ test Perform unit testing \\ run Create executable and run immediately \\ @@ -263,6 +264,8 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { } else if (mem.eql(u8, cmd, "build-obj")) { dev.check(.build_obj_command); return buildOutputType(gpa, arena, args, .{ .build = .Obj }); + } else if (mem.eql(u8, cmd, "build-pch")) { + return buildOutputType(gpa, arena, args, .pch); } else if (mem.eql(u8, cmd, "test")) { dev.check(.test_command); return buildOutputType(gpa, arena, args, .zig_test); @@ -380,6 +383,7 @@ const usage_build_generic = \\Usage: zig build-exe [options] [files] \\ zig build-lib [options] [files] \\ zig build-obj [options] [files] + \\ zig build-pch [options] [files] \\ zig test [options] [files] \\ zig run [options] [files] [-- [args]] \\ zig translate-c [options] [file] @@ -733,6 +737,7 @@ const ArgMode = union(enum) { build: std.builtin.OutputMode, cc, cpp, + pch, translate_c, zig_test, run, @@ -999,11 +1004,15 @@ fn buildOutputType( var n_jobs: ?u32 = null; switch (arg_mode) { - .build, .translate_c, .zig_test, .run => { + .build, .translate_c, .zig_test, .run, .pch => { switch (arg_mode) { .build => |m| { create_module.opts.output_mode = m; }, + .pch => { + create_module.opts.output_mode = .Obj; + clang_preprocessor_mode = .pch; + }, .translate_c => { emit_bin = .no; create_module.opts.output_mode = .Obj; diff --git a/test/link/link.zig b/test/link/link.zig index 4537846353..11d582de19 100644 --- a/test/link/link.zig +++ b/test/link/link.zig @@ -119,7 +119,7 @@ fn addCompileStep( }); } if (overlay.asm_source_bytes) |bytes| { - compile_step.addAssemblyFile(b.addWriteFiles().add("a.s", bytes)); + compile_step.addAssemblyFile(.{ .file = b.addWriteFiles().add("a.s", bytes) }); } return compile_step; } @@ -147,7 +147,7 @@ pub fn addAsmSourceBytes(comp: *Compile, bytes: []const u8) void { const b = comp.step.owner; const actual_bytes = std.fmt.allocPrint(b.allocator, "{s}\n", .{bytes}) catch @panic("OOM"); const file = WriteFile.create(b).add("a.s", actual_bytes); - comp.addAssemblyFile(file); + comp.addAssemblyFile(.{ .file = file }); } pub fn expectLinkErrors(comp: *Compile, test_step: *Step, expected_errors: Compile.ExpectedCompileErrors) void { diff --git a/test/src/CompareOutput.zig b/test/src/CompareOutput.zig index 58c1238705..46edb391bd 100644 --- a/test/src/CompareOutput.zig +++ b/test/src/CompareOutput.zig @@ -101,7 +101,7 @@ pub fn addCase(self: *CompareOutput, case: TestCase) void { .target = b.graph.host, .optimize = .Debug, }); - exe.addAssemblyFile(first_file); + exe.addAssemblyFile(.{ .file = first_file }); const run = b.addRunArtifact(exe); run.setName(annotated_case_name); diff --git a/test/standalone/build.zig.zon b/test/standalone/build.zig.zon index 30ec07823b..96c7b2198e 100644 --- a/test/standalone/build.zig.zon +++ b/test/standalone/build.zig.zon @@ -126,6 +126,9 @@ .c_compiler = .{ .path = "c_compiler", }, + .c_header = .{ + .path = "c_header", + }, .pie = .{ .path = "pie", }, diff --git a/test/standalone/c_header/build.zig b/test/standalone/c_header/build.zig new file mode 100644 index 0000000000..b7c73e6480 --- /dev/null +++ b/test/standalone/c_header/build.zig @@ -0,0 +1,91 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const test_step = b.step("test", "Test it"); + b.default_step = test_step; + + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + // testcase 1: single-file c-header library + { + const exe = b.addExecutable(.{ + .name = "single-file-library", + .root_source_file = b.path("main.zig"), + .target = target, + .optimize = optimize, + }); + + exe.linkLibC(); + exe.addIncludePath(b.path(".")); + exe.addCSourceFile(.{ + .file = b.path("single_file_library.h"), + .lang = .c, + .flags = &.{"-DTSTLIB_IMPLEMENTATION"}, + }); + + test_step.dependOn(&b.addRunArtifact(exe).step); + } + + // testcase 2: precompiled header in C, from a generated file, with a compile step generated automaticcaly, twice with a cache hit + // and it also test the explicit source lang not inferred from file extenson. + { + const exe = b.addExecutable(.{ + .name = "pchtest", + .target = target, + .optimize = optimize, + .link_libc = true, + }); + + const generated_header = b.addWriteFiles().add("generated.h", + \\ /* generated file */ + \\ #include "include_a.h" + ); + + exe.addCSourceFile(.{ + .file = b.path("test.c2"), + .flags = &[_][]const u8{}, + .lang = .c, + .precompiled_header = .{ .source_header = .{ .path = generated_header, .lang = .h } }, + }); + exe.addCSourceFiles(.{ + .files = &.{"test.c"}, + .flags = &[_][]const u8{}, + .lang = .c, + .precompiled_header = .{ .source_header = .{ .path = generated_header, .lang = .h } }, + }); + + exe.addIncludePath(b.path(".")); + + test_step.dependOn(&b.addRunArtifact(exe).step); + } + + // testcase 3: precompiled header in C++, from a .h file that must be precompiled as c++, with an explicit pch compile step. + { + const exe = b.addExecutable(.{ + .name = "pchtest++", + .target = target, + .optimize = optimize, + }); + exe.linkLibCpp(); + + const pch = b.addPrecompiledCHeader(.{ + .name = "pch_c++", + .target = target, + .optimize = optimize, + .link_libcpp = true, + }, .{ + .file = b.path("include_a.h"), + .flags = &[_][]const u8{}, + .lang = .hpp, + }); + + exe.addCSourceFile(.{ + .file = b.path("test.cpp"), + .flags = &[_][]const u8{}, + .precompiled_header = .{ .pch_step = pch }, + }); + + test_step.dependOn(&b.addRunArtifact(exe).step); + } +} diff --git a/test/standalone/c_header/include_a.h b/test/standalone/c_header/include_a.h new file mode 100644 index 0000000000..9b092aec5a --- /dev/null +++ b/test/standalone/c_header/include_a.h @@ -0,0 +1,16 @@ +#pragma once + +#include "include_b.h" + +#include +#include + +#if defined(__cplusplus) +#include +#else +#include +#include +#endif + +#define A_INCLUDED 1 + diff --git a/test/standalone/c_header/include_b.h b/test/standalone/c_header/include_b.h new file mode 100644 index 0000000000..13806ca667 --- /dev/null +++ b/test/standalone/c_header/include_b.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +typedef double real; + +#define B_INCLUDED 1 diff --git a/test/standalone/c_header/main.zig b/test/standalone/c_header/main.zig new file mode 100644 index 0000000000..9c53d9e3b4 --- /dev/null +++ b/test/standalone/c_header/main.zig @@ -0,0 +1,11 @@ +const std = @import("std"); +const C = @cImport({ + @cInclude("single_file_library.h"); +}); + +pub fn main() !void { + const msg = "hello"; + const val = C.tstlib_len(msg); + if (val != msg.len) + std.process.exit(1); +} diff --git a/test/standalone/c_header/single_file_library.h b/test/standalone/c_header/single_file_library.h new file mode 100644 index 0000000000..b87e496c73 --- /dev/null +++ b/test/standalone/c_header/single_file_library.h @@ -0,0 +1,14 @@ +// library header: +extern unsigned tstlib_len(const char* msg); + +// library implementation: +#ifdef TSTLIB_IMPLEMENTATION + +#include + +unsigned tstlib_len(const char* msg) +{ + return strlen(msg); +} + +#endif diff --git a/test/standalone/c_header/test.c b/test/standalone/c_header/test.c new file mode 100644 index 0000000000..5add8667bf --- /dev/null +++ b/test/standalone/c_header/test.c @@ -0,0 +1,19 @@ + +// includes commented out to make sure the symbols come from the precompiled header. +//#include "include_a.h" +//#include "include_b.h" + +#ifndef A_INCLUDED +#error "pch not included" +#endif +#ifndef B_INCLUDED +#error "pch not included" +#endif + +extern int func(real a, bool cond); + +int main(int argc, char *argv[]) +{ + real a = 0.123; + return func(a, (argc > 1)); +} diff --git a/test/standalone/c_header/test.c2 b/test/standalone/c_header/test.c2 new file mode 100644 index 0000000000..033ec00b27 --- /dev/null +++ b/test/standalone/c_header/test.c2 @@ -0,0 +1,20 @@ + +// includes commented out to make sure the symbols come from the precompiled header. +//#include "include_a.h" +//#include "include_b.h" + +#ifndef A_INCLUDED +#error "pch not included" +#endif +#ifndef B_INCLUDED +#error "pch not included" +#endif + +int func(real a, bool cond) +{ + if (cond) { + fprintf(stdout, "abs(%g)=%g\n", a, fabs(a)); + } + + return EXIT_SUCCESS; +} diff --git a/test/standalone/c_header/test.cpp b/test/standalone/c_header/test.cpp new file mode 100644 index 0000000000..ebea93a565 --- /dev/null +++ b/test/standalone/c_header/test.cpp @@ -0,0 +1,23 @@ + +// includes commented out to make sure the symbols come from the precompiled header. +//#include "includeA.h" +//#include "includeB.h" + +#ifndef A_INCLUDED +#error "pch not included" +#endif +#ifndef B_INCLUDED +#error "pch not included" +#endif + +int main(int argc, char *argv[]) +{ + real a = -0.123; + + if (argc > 1) { + std::cout << "abs(" << a << ")=" << fabs(a) << "\n"; + } + + return EXIT_SUCCESS; +} +