Skip to content

add precompiled c/c++ header support to zig build #17956

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
57 changes: 52 additions & 5 deletions lib/compiler/build_runner.zig
Original file line number Diff line number Diff line change
@@ -340,6 +340,10 @@ pub fn main() !void {
createModuleDependencies(builder) catch @panic("OOM");
}

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| {
@@ -637,6 +641,7 @@ fn runStepNames(
test_count += s.test_results.test_count;

switch (s.state) {
.unfinalized => unreachable,
.precheck_unstarted => unreachable,
.precheck_started => unreachable,
.running => unreachable,
@@ -782,6 +787,7 @@ fn printStepStatus(
run: *const Run,
) !void {
switch (s.state) {
.unfinalized => unreachable,
.precheck_unstarted => unreachable,
.precheck_started => unreachable,
.precheck_done => unreachable,
@@ -979,6 +985,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
@@ -997,6 +1031,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;
@@ -1059,6 +1094,7 @@ fn workerMakeOneStep(
// dependency is not finished yet.
return;
},
.unfinalized => unreachable,
.precheck_unstarted => unreachable,
.precheck_started => unreachable,
}
@@ -1504,13 +1540,24 @@ fn createModuleDependenciesForStep(step: *Step) Allocator.Error!void {
.special => {},
};
for (mod.link_objects.items) |link_object| switch (link_object) {
.static_path,
.assembly_file,
=> |lp| lp.addStepDependencies(step),
.static_path => |lp| lp.addStepDependencies(step),
.assembly_file => |source| source.file.addStepDependencies(step),
.other_step => |other| step.dependOn(&other.step),
.system_lib => {},
.c_source_file => |source| source.file.addStepDependencies(step),
.c_source_files => |source_files| source_files.root.addStepDependencies(step),
.c_source_file => |source| {
source.file.addStepDependencies(step);
if (source.precompiled_header) |pch| switch (pch) {
.source_header => |src| src.path.addStepDependencies(step),
.pch_step => |s| step.dependOn(&s.step),
};
},
.c_source_files => |source_files| {
source_files.root.addStepDependencies(step);
if (source_files.precompiled_header) |pch| switch (pch) {
.source_header => |src| src.path.addStepDependencies(step),
.pch_step => |s| step.dependOn(&s.step),
};
},
.win32_resource_file => |rc_source| {
rc_source.file.addStepDependencies(step);
for (rc_source.include_paths) |lp| lp.addStepDependencies(step);
25 changes: 25 additions & 0 deletions lib/std/Build.zig
Original file line number Diff line number Diff line change
@@ -828,6 +828,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 = b.createModule(.{
.target = options.target,
.optimize = options.optimize,
.link_libc = options.link_libc,
.link_libcpp = options.link_libcpp,
}),
.kind = .pch,
.max_rss = options.max_rss,
});
pch.root_module.addCSourceFile(source);
return pch;
}

pub const SharedLibraryOptions = struct {
name: []const u8,
version: ?std.SemanticVersion = null,
120 changes: 117 additions & 3 deletions lib/std/Build/Module.zig
Original file line number Diff line number Diff line change
@@ -46,11 +46,39 @@ pub const RPath = union(enum) {
special: []const u8,
};

// subset of Compilation.FileExt
pub const AsmSourceLang = enum {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should there be a way to specify defines and/or flags? When using assembly with the C preprocessor, defines are definitely important; I'm not sure if general purpose flags would be of any use though, and defines are only used in assembly with the C preprocessor.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess so, but I never used addAssemblyFile() myself so I don't know how it is used by people. And I don't know if there is a reason to have it separate from addCSourceFile() - which coincidentally also allows for asm files with defines and all.
maybe it should just be removed?

Altough I don't want to include unrelated changes to the pull request, looks like I still felt compelled to add AsmSourceFile to make the API of addAssemblyFile() more similar to addCSourceFile() for some reason, maybe I shouldn't have or I should go all the way. (but that looks like wasted effort if it is to be deleted)

assembly,
assembly_with_cpp,

pub fn getName(lang: @This()) []const u8 {
return switch (lang) {
.assembly => "assembler",
.assembly_with_cpp => "assembler-with-cpp",
};
}
};

pub const AsmSourceFile = struct {
file: LazyPath,
/// if null, deduce the language from the file extension
lang: ?AsmSourceLang = null,
flags: []const []const u8 = &.{},

pub fn dupe(file: AsmSourceFile, b: *std.Build) AsmSourceFile {
return .{
.file = file.file.dupe(b),
.flags = b.dupeStrings(file.flags),
.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 +106,88 @@ pub const SystemLib = struct {
pub const SearchStrategy = enum { paths_first, mode_first, no_fallback };
};

/// Supported languages for "zig clang -x <lang>".
// 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,
Comment on lines +128 to +131
Copy link
Contributor

Choose a reason for hiding this comment

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

If there is already an addAssemblyFile, is .assembly and .assembly_with_cpp necessary in CSourceLang? The only difference is the ability to provide flags and/or defines (which may be important). We probably don't want two ways to do the same thing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, as I said I'm more drawn to removing it. and maybe finding a better name for addCSourceFile to reflect that it is not "C" source, but "not zig" source?

Copy link
Contributor

Choose a reason for hiding this comment

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

Relevant comment about the name: #20655 (comment)

Zig primarily has built-in first-class support for the C and C-like languages, and I don't believe there is intention to have that kind of support for other languages, so addCSourceFile may be fine.

/// "cuda"
cu,

pub fn getName(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,
/// if null, deduce the language from the file extension
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,
};
}
};
@@ -377,7 +471,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.
@@ -398,9 +494,18 @@ 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");

if (options.precompiled_header) |pch| {
switch (pch) {
.source_header => {},
.pch_step => |step| _ = step.getEmittedBin(), // Indicate there is a dependency on the outputted binary.
}
}
}

pub fn addCSourceFile(m: *Module, source: CSourceFile) void {
@@ -409,6 +514,13 @@ pub fn addCSourceFile(m: *Module, source: CSourceFile) void {
const c_source_file = allocator.create(CSourceFile) catch @panic("OOM");
c_source_file.* = source.dupe(b);
m.link_objects.append(allocator, .{ .c_source_file = c_source_file }) catch @panic("OOM");

if (source.precompiled_header) |pch| {
switch (pch) {
.source_header => {},
.pch_step => |step| _ = step.getEmittedBin(), // Indicate there is a dependency on the outputted binary.
}
}
}

/// Resource files must have the extension `.rc`.
@@ -427,9 +539,11 @@ pub fn addWin32ResourceFile(m: *Module, source: RcSourceFile) void {
m.link_objects.append(allocator, .{ .win32_resource_file = rc_source_file }) catch @panic("OOM");
}

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");
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");
}

pub fn addObjectFile(m: *Module, object: LazyPath) void {
17 changes: 16 additions & 1 deletion lib/std/Build/Step.zig
Original file line number Diff line number Diff line change
@@ -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;

Loading
Loading