From f37ee3f767e61f1e215e3e3b4e07b76b46d6797e Mon Sep 17 00:00:00 2001 From: Xavier Bouchoux Date: Fri, 10 Nov 2023 14:20:05 +0100 Subject: [PATCH] std.Build: add precompiled C header support v2: add `-fpch-validate-input-files-content` "Validate PCH input files based on content if mtime differs" so that llvm check matches zig caching behaviour. --- lib/std/Build.zig | 23 ++++++++ lib/std/Build/Step/Compile.zig | 78 ++++++++++++++++++++------ lib/std/Build/Step/InstallArtifact.zig | 2 +- test/standalone.zig | 4 ++ test/standalone/pch/build.zig | 69 +++++++++++++++++++++++ test/standalone/pch/include_a.h | 15 +++++ test/standalone/pch/include_b.h | 7 +++ test/standalone/pch/test.c | 22 ++++++++ test/standalone/pch/test.cpp | 23 ++++++++ 9 files changed, 225 insertions(+), 18 deletions(-) create mode 100644 test/standalone/pch/build.zig create mode 100644 test/standalone/pch/include_a.h create mode 100644 test/standalone/pch/include_b.h create mode 100644 test/standalone/pch/test.c create mode 100644 test/standalone/pch/test.cpp diff --git a/lib/std/Build.zig b/lib/std/Build.zig index 9b1677c40fcc..f26babe765b4 100644 --- a/lib/std/Build.zig +++ b/lib/std/Build.zig @@ -700,6 +700,29 @@ pub fn addObject(b: *Build, options: ObjectOptions) *Step.Compile { }); } +pub const PchOptions = struct { + name: []const u8, + target: CrossTarget, + optimize: std.builtin.OptimizeMode, + max_rss: usize = 0, + cpp_header: bool = false, +}; + +pub fn addPrecompiledCHeader(b: *Build, options: PchOptions, source: Step.Compile.CSourceFile) *Step.Compile { + const pch = Step.Compile.create(b, .{ + .name = options.name, + .target = options.target, + .optimize = options.optimize, + .kind = .pch, + .max_rss = options.max_rss, + .use_llvm = true, + }); + pch.is_linking_libc = !options.cpp_header; + pch.is_linking_libcpp = options.cpp_header; + pch.addCSourceFile(source); + return pch; +} + pub const SharedLibraryOptions = struct { name: []const u8, root_source_file: ?LazyPath = null, diff --git a/lib/std/Build/Step/Compile.zig b/lib/std/Build/Step/Compile.zig index d06a44c4da47..e5284e5b6762 100644 --- a/lib/std/Build/Step/Compile.zig +++ b/lib/std/Build/Step/Compile.zig @@ -83,6 +83,7 @@ root_src: ?LazyPath, out_lib_filename: []const u8, modules: std.StringArrayHashMap(*Module), +precompiled_header: ?*Compile, link_objects: ArrayList(LinkObject), include_dirs: ArrayList(IncludeDir), c_macros: ArrayList([]const u8), @@ -439,6 +440,7 @@ pub const Kind = enum { exe, lib, obj, + pch, @"test", }; @@ -462,6 +464,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, @@ -471,20 +474,24 @@ pub fn create(owner: *std.Build, options: Options) *Compile { const target_info = NativeTargetInfo.detect(options.target) catch @panic("unhandled error"); - const out_filename = std.zig.binNameAlloc(owner.allocator, .{ - .root_name = name, - .target = target_info.target, - .output_mode = switch (options.kind) { - .lib => .Lib, - .obj => .Obj, - .exe, .@"test" => .Exe, - }, - .link_mode = if (options.linkage) |some| @as(std.builtin.LinkMode, switch (some) { - .dynamic => .Dynamic, - .static => .Static, - }) else null, - .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_info.target, + .output_mode = switch (options.kind) { + .lib => .Lib, + .obj => .Obj, + .exe, .@"test" => .Exe, + .pch => unreachable, + }, + .link_mode = if (options.linkage) |some| @as(std.builtin.LinkMode, switch (some) { + .dynamic => .Dynamic, + .static => .Static, + }) else null, + .version = options.version, + }) catch @panic("OOM"); const self = owner.allocator.create(Compile) catch @panic("OOM"); self.* = .{ @@ -518,6 +525,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile { .lib_paths = ArrayList(LazyPath).init(owner.allocator), .rpaths = ArrayList(LazyPath).init(owner.allocator), .installed_headers = ArrayList(*Step).init(owner.allocator), + .precompiled_header = null, .c_std = std.Build.CStd.C99, .zig_lib_dir = null, .main_mod_path = null, @@ -980,6 +988,8 @@ pub const AddCSourceFilesOptions = struct { /// Handy when you have many C/C++ source files and want them all to have the same flags. pub fn addCSourceFiles(self: *Compile, options: AddCSourceFilesOptions) void { + assert(self.kind != .pch); // pch can only be generated from a single C header file + const b = self.step.owner; const c_source_files = b.allocator.create(CSourceFiles) catch @panic("OOM"); @@ -995,6 +1005,8 @@ pub fn addCSourceFiles(self: *Compile, options: AddCSourceFilesOptions) void { } pub fn addCSourceFile(self: *Compile, source: CSourceFile) void { + assert(self.kind != .pch or self.link_objects.items.len == 0); // pch can only be generated from a single C header file + const b = self.step.owner; const c_source_file = b.allocator.create(CSourceFile) catch @panic("OOM"); c_source_file.* = source.dupe(b); @@ -1111,6 +1123,8 @@ pub fn getEmittedLlvmBc(self: *Compile) LazyPath { } pub fn addAssemblyFile(self: *Compile, source: LazyPath) void { + assert(self.kind != .pch); // pch can only be generated from a single C header file + const b = self.step.owner; const source_duped = source.dupe(b); self.link_objects.append(.{ .assembly_file = source_duped }) catch @panic("OOM"); @@ -1118,16 +1132,30 @@ pub fn addAssemblyFile(self: *Compile, source: LazyPath) void { } pub fn addObjectFile(self: *Compile, source: LazyPath) void { + assert(self.kind != .pch); // pch can only be generated from a single C header file + const b = self.step.owner; self.link_objects.append(.{ .static_path = source.dupe(b) }) catch @panic("OOM"); source.addStepDependencies(&self.step); } pub fn addObject(self: *Compile, obj: *Compile) void { + assert(self.kind != .pch); // pch can only be generated from a single C header file + assert(obj.kind == .obj); self.linkLibraryOrObject(obj); } +pub fn addPrecompiledCHeader(self: *Compile, pch: *Compile) void { + assert(self.kind != .pch); // pch can only be generated from a single C header file + assert(pch.kind == .pch); + + if (self.precompiled_header != null) @panic("Precompiled header already defined."); + self.precompiled_header = pch; + + pch.getEmittedBin().addStepDependencies(&self.step); +} + pub fn addAfterIncludePath(self: *Compile, path: LazyPath) void { const b = self.step.owner; self.include_dirs.append(IncludeDir{ .path_after = path.dupe(b) }) catch @panic("OOM"); @@ -1419,7 +1447,7 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void { const cmd = switch (self.kind) { .lib => "build-lib", .exe => "build-exe", - .obj => "build-obj", + .obj, .pch => "build-obj", .@"test" => "test", }; try zig_args.append(cmd); @@ -1435,6 +1463,11 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void { try zig_args.append(try std.fmt.allocPrint(b.allocator, "-ofmt={s}", .{@tagName(ofmt)})); } + if (self.kind == .pch) { + try zig_args.append("-x"); + try zig_args.append(if (self.is_linking_libcpp) "c++-header" else "c-header"); + } + switch (self.entry) { .default => {}, .disabled => try zig_args.append("-fno-entry"), @@ -1486,6 +1519,7 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void { .other_step => |other| switch (other.kind) { .exe => @panic("Cannot link with an executable build artifact"), .@"test" => @panic("Cannot link with a test"), + .pch => @panic("Cannot link with a precompiled header file"), .obj => { try zig_args.append(other.getEmittedBin().getPath(b)); }, @@ -1582,7 +1616,7 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void { }, .c_source_file => |c_source_file| { - if (c_source_file.flags.len == 0) { + if (c_source_file.flags.len == 0 and self.precompiled_header == null) { if (prev_has_cflags) { try zig_args.append("-cflags"); try zig_args.append("--"); @@ -1593,6 +1627,11 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void { for (c_source_file.flags) |arg| { try zig_args.append(arg); } + if (self.precompiled_header) |pch| { + try zig_args.append("-include-pch"); + try zig_args.append(pch.getEmittedBin().getPath(b)); + try zig_args.append("-fpch-validate-input-files-content"); + } try zig_args.append("--"); prev_has_cflags = true; } @@ -1600,7 +1639,7 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void { }, .c_source_files => |c_source_files| { - if (c_source_files.flags.len == 0) { + if (c_source_files.flags.len == 0 and self.precompiled_header == null) { if (prev_has_cflags) { try zig_args.append("-cflags"); try zig_args.append("--"); @@ -1611,6 +1650,11 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void { for (c_source_files.flags) |flag| { try zig_args.append(flag); } + if (self.precompiled_header) |pch| { + try zig_args.append("-include-pch"); + try zig_args.append(pch.getEmittedBin().getPath(b)); + try zig_args.append("-fpch-validate-input-files-content"); + } try zig_args.append("--"); prev_has_cflags = true; } diff --git a/lib/std/Build/Step/InstallArtifact.zig b/lib/std/Build/Step/InstallArtifact.zig index f3c9ca3bef0e..b87a58fb98e3 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" => InstallDir{ .bin = {} }, .lib => InstallDir{ .lib = {} }, }, diff --git a/test/standalone.zig b/test/standalone.zig index fc455a2aa71c..a5414aa897f8 100644 --- a/test/standalone.zig +++ b/test/standalone.zig @@ -262,6 +262,10 @@ pub const build_cases = [_]BuildCase{ .build_root = "test/standalone/ios", .import = @import("standalone/ios/build.zig"), }, + .{ + .build_root = "test/standalone/pch", + .import = @import("standalone/pch/build.zig"), + }, }; const std = @import("std"); diff --git a/test/standalone/pch/build.zig b/test/standalone/pch/build.zig new file mode 100644 index 000000000000..c5423846decd --- /dev/null +++ b/test/standalone/pch/build.zig @@ -0,0 +1,69 @@ +const std = @import("std"); +const Builder = std.build.Builder; + +pub fn build(b: *Builder) void { + const test_step = b.step("test", "Test it"); + b.default_step = test_step; + + const target = b.standardTargetOptions(.{}); + const mode = b.standardOptimizeOption(.{}); + + // c-header + { + const exe = b.addExecutable(.{ + .name = "pchtest", + .target = target, + .optimize = mode, + .link_libc = true, + }); + + const pch = b.addPrecompiledCHeader(.{ + .name = "pch_c", + .target = target, + .optimize = mode, + .cpp_header = false, + }, .{ + .file = .{ .path = "include_a.h" }, + .flags = &[_][]const u8{}, + }); + + exe.addPrecompiledCHeader(pch); + + exe.addCSourceFile(.{ + .file = .{ .path = "test.c" }, + .flags = &[_][]const u8{}, + }); + + test_step.dependOn(&b.addRunArtifact(exe).step); + } + + // c++-header + { + const exe = b.addExecutable(.{ + .name = "pchtest++", + .target = target, + .optimize = mode, + .link_libc = true, + }); + exe.linkLibCpp(); + + const pch = b.addPrecompiledCHeader(.{ + .name = "pch_c++", + .target = target, + .optimize = mode, + .cpp_header = true, + }, .{ + .file = .{ .path = "include_a.h" }, + .flags = &[_][]const u8{}, + }); + + exe.addPrecompiledCHeader(pch); + + exe.addCSourceFile(.{ + .file = .{ .path = "test.cpp" }, + .flags = &[_][]const u8{}, + }); + + test_step.dependOn(&b.addRunArtifact(exe).step); + } +} diff --git a/test/standalone/pch/include_a.h b/test/standalone/pch/include_a.h new file mode 100644 index 000000000000..42921826c8a7 --- /dev/null +++ b/test/standalone/pch/include_a.h @@ -0,0 +1,15 @@ +#pragma once + +#include "include_b.h" + +#include +#include + +#if defined(__cplusplus) +#include +#else +#include +#endif + +#define A_INCLUDED 1 + diff --git a/test/standalone/pch/include_b.h b/test/standalone/pch/include_b.h new file mode 100644 index 000000000000..13806ca6672c --- /dev/null +++ b/test/standalone/pch/include_b.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +typedef double real; + +#define B_INCLUDED 1 diff --git a/test/standalone/pch/test.c b/test/standalone/pch/test.c new file mode 100644 index 000000000000..608083cf5010 --- /dev/null +++ b/test/standalone/pch/test.c @@ -0,0 +1,22 @@ + +// 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) { + fprintf(stdout, "abs(%g)=%g\n", a, abs(a)); + } + + return EXIT_SUCCESS; +} diff --git a/test/standalone/pch/test.cpp b/test/standalone/pch/test.cpp new file mode 100644 index 000000000000..ebea93a565a5 --- /dev/null +++ b/test/standalone/pch/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; +} +