mirror of
https://github.com/roc-lang/roc.git
synced 2025-12-23 08:48:03 +00:00
Zig's archive generator doesn't add the required padding byte after odd-sized members, causing lld to reject the archive. This adds a build step that appends the missing padding byte when needed. See https://codeberg.org/ziglang/zig/issues/30572 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
3347 lines
130 KiB
Zig
3347 lines
130 KiB
Zig
const std = @import("std");
|
|
const builtin = @import("builtin");
|
|
const modules = @import("src/build/modules.zig");
|
|
const glibc_stub_build = @import("src/build/glibc_stub.zig");
|
|
const roc_target = @import("src/target/mod.zig");
|
|
const Dependency = std.Build.Dependency;
|
|
const Import = std.Build.Module.Import;
|
|
const InstallDir = std.Build.InstallDir;
|
|
const LazyPath = std.Build.LazyPath;
|
|
const OptimizeMode = std.builtin.OptimizeMode;
|
|
const ResolvedTarget = std.Build.ResolvedTarget;
|
|
const Step = std.Build.Step;
|
|
|
|
// Cross-compile target definitions
|
|
|
|
/// Cross-compile target specification
|
|
const CrossTarget = struct {
|
|
name: []const u8,
|
|
query: std.Target.Query,
|
|
};
|
|
|
|
/// Musl-only cross-compile targets (static linking)
|
|
const musl_cross_targets = [_]CrossTarget{
|
|
.{ .name = "x64musl", .query = .{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .musl } },
|
|
.{ .name = "arm64musl", .query = .{ .cpu_arch = .aarch64, .os_tag = .linux, .abi = .musl } },
|
|
};
|
|
|
|
/// Glibc cross-compile targets (dynamic linking)
|
|
const glibc_cross_targets = [_]CrossTarget{
|
|
.{ .name = "x64glibc", .query = .{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .gnu } },
|
|
.{ .name = "arm64glibc", .query = .{ .cpu_arch = .aarch64, .os_tag = .linux, .abi = .gnu } },
|
|
};
|
|
|
|
/// Windows cross-compile targets
|
|
const windows_cross_targets = [_]CrossTarget{
|
|
.{ .name = "x64win", .query = .{ .cpu_arch = .x86_64, .os_tag = .windows, .abi = .msvc } },
|
|
.{ .name = "arm64win", .query = .{ .cpu_arch = .aarch64, .os_tag = .windows, .abi = .msvc } },
|
|
};
|
|
|
|
/// All Linux cross-compile targets (musl + glibc)
|
|
const linux_cross_targets = musl_cross_targets ++ glibc_cross_targets;
|
|
|
|
/// Test platform directories that need host libraries built
|
|
const all_test_platform_dirs = [_][]const u8{ "str", "int", "fx", "fx-open" };
|
|
|
|
fn mustUseLlvm(target: ResolvedTarget) bool {
|
|
return target.result.os.tag == .macos and target.result.cpu.arch == .x86_64;
|
|
}
|
|
|
|
fn configureBackend(step: *Step.Compile, target: ResolvedTarget) void {
|
|
if (mustUseLlvm(target)) {
|
|
step.use_llvm = true;
|
|
}
|
|
}
|
|
|
|
fn isNativeishOrMusl(target: ResolvedTarget) bool {
|
|
return target.result.cpu.arch == builtin.target.cpu.arch and
|
|
target.query.isNativeOs() and
|
|
(target.query.isNativeAbi() or target.result.abi.isMusl());
|
|
}
|
|
|
|
const TestsSummaryStep = struct {
|
|
step: Step,
|
|
has_filters: bool,
|
|
forced_passes: u64,
|
|
|
|
fn create(
|
|
b: *std.Build,
|
|
test_filters: []const []const u8,
|
|
forced_passes: usize,
|
|
) *TestsSummaryStep {
|
|
const self = b.allocator.create(TestsSummaryStep) catch @panic("OOM");
|
|
self.* = .{
|
|
.step = Step.init(.{
|
|
.id = Step.Id.custom,
|
|
.name = "tests_summary",
|
|
.owner = b,
|
|
.makeFn = make,
|
|
}),
|
|
.has_filters = test_filters.len > 0,
|
|
.forced_passes = @intCast(forced_passes),
|
|
};
|
|
return self;
|
|
}
|
|
|
|
fn addRun(self: *TestsSummaryStep, run_step: *Step) void {
|
|
self.step.dependOn(run_step);
|
|
}
|
|
|
|
fn make(step: *Step, options: Step.MakeOptions) !void {
|
|
_ = options;
|
|
|
|
const self: *TestsSummaryStep = @fieldParentPtr("step", step);
|
|
|
|
var passed: u64 = 0;
|
|
|
|
for (step.dependencies.items) |dependency| {
|
|
const module_pass_count = dependency.test_results.passCount();
|
|
passed += @intCast(module_pass_count);
|
|
}
|
|
|
|
var effective_passed = passed;
|
|
if (self.has_filters and self.forced_passes != 0) {
|
|
const subtract = @min(effective_passed, self.forced_passes);
|
|
effective_passed -= subtract;
|
|
}
|
|
|
|
if (effective_passed == 0) {
|
|
std.debug.print("No tests ran (all tests filtered out).\n", .{});
|
|
} else {
|
|
std.debug.print("All {d} tests passed.\n", .{effective_passed});
|
|
}
|
|
}
|
|
};
|
|
|
|
/// Build step that checks for forbidden patterns in the type checker code.
|
|
///
|
|
/// During type checking, we NEVER do string or byte comparisons because:
|
|
/// 1. They take linear time, which can cause performance issues
|
|
/// 2. They are brittle to changes that type-checking should not be sensitive to
|
|
///
|
|
/// Instead, we always compare indices - either into node stores or to interned string indices.
|
|
/// This step enforces that rule by failing the build if `std.mem.` is found in src/canonicalize/, src/check/, src/layout/, or src/eval/.
|
|
const CheckTypeCheckerPatternsStep = struct {
|
|
step: Step,
|
|
|
|
fn create(b: *std.Build) *CheckTypeCheckerPatternsStep {
|
|
const self = b.allocator.create(CheckTypeCheckerPatternsStep) catch @panic("OOM");
|
|
self.* = .{
|
|
.step = Step.init(.{
|
|
.id = Step.Id.custom,
|
|
.name = "check-type-checker-patterns",
|
|
.owner = b,
|
|
.makeFn = make,
|
|
}),
|
|
};
|
|
return self;
|
|
}
|
|
|
|
fn make(step: *Step, _: Step.MakeOptions) !void {
|
|
const b = step.owner;
|
|
const allocator = b.allocator;
|
|
|
|
var violations = std.ArrayList(Violation).empty;
|
|
defer violations.deinit(allocator);
|
|
|
|
// Recursively scan src/canonicalize/, src/check/, src/layout/, and src/eval/ for .zig files
|
|
// TODO: uncomment "src/canonicalize" once its std.mem violations are fixed
|
|
const dirs_to_scan = [_][]const u8{ "src/check", "src/layout", "src/eval" };
|
|
for (dirs_to_scan) |dir_path| {
|
|
var dir = std.fs.cwd().openDir(dir_path, .{ .iterate = true }) catch |err| {
|
|
return step.fail("Failed to open {s} directory: {}", .{ dir_path, err });
|
|
};
|
|
defer dir.close();
|
|
|
|
try scanDirectory(allocator, dir, dir_path, &violations);
|
|
}
|
|
|
|
if (violations.items.len > 0) {
|
|
std.debug.print("\n", .{});
|
|
std.debug.print("=" ** 80 ++ "\n", .{});
|
|
std.debug.print("FORBIDDEN PATTERN DETECTED\n", .{});
|
|
std.debug.print("=" ** 80 ++ "\n\n", .{});
|
|
|
|
std.debug.print(
|
|
\\Code in src/canonicalize/, src/check/, src/layout/, and src/eval/ must NOT do raw string comparison or manipulation.
|
|
\\
|
|
\\WHY THIS RULE EXISTS:
|
|
\\ We NEVER do string or byte comparisons because:
|
|
\\
|
|
\\ 1. PERFORMANCE: String comparisons take O(n) time where n is the string
|
|
\\ length. These code paths can involve many comparisons, so this adds up.
|
|
\\
|
|
\\ 2. BRITTLENESS: String comparisons make the code sensitive to changes it
|
|
\\ shouldn't care about (e.g., how identifiers are rendered, whitespace,
|
|
\\ formatting). This leads to subtle bugs.
|
|
\\
|
|
\\WHAT TO DO INSTEAD:
|
|
\\ Always compare indices rather than strings:
|
|
\\
|
|
\\ - For identifiers: Compare Ident.Idx values (interned string indices)
|
|
\\ - For types: Compare type variable indices or node store indices
|
|
\\ - For expressions: Compare Expr.Idx values from the node store
|
|
\\
|
|
\\ Example - WRONG:
|
|
\\ if (std.mem.eql(u8, ident_name, "is_eq")) {{ ... }}
|
|
\\
|
|
\\ Example - RIGHT:
|
|
\\ if (ident_idx == module_env.idents.is_eq) {{ ... }}
|
|
\\
|
|
\\VIOLATIONS FOUND:
|
|
\\
|
|
, .{});
|
|
|
|
for (violations.items) |violation| {
|
|
std.debug.print(" {s}:{d}: {s}\n", .{
|
|
violation.file_path,
|
|
violation.line_number,
|
|
violation.line_content,
|
|
});
|
|
}
|
|
|
|
std.debug.print("\n" ++ "=" ** 80 ++ "\n", .{});
|
|
|
|
return step.fail(
|
|
"Found {d} forbidden patterns (raw string comparison or manipulation) in src/canonicalize/, src/check/, src/layout/, or src/eval/. " ++
|
|
"See above for details on why this is forbidden and what to do instead.",
|
|
.{violations.items.len},
|
|
);
|
|
}
|
|
}
|
|
|
|
const Violation = struct {
|
|
file_path: []const u8,
|
|
line_number: usize,
|
|
line_content: []const u8,
|
|
};
|
|
|
|
fn scanDirectory(
|
|
allocator: std.mem.Allocator,
|
|
dir: std.fs.Dir,
|
|
path_prefix: []const u8,
|
|
violations: *std.ArrayList(Violation),
|
|
) !void {
|
|
var walker = try dir.walk(allocator);
|
|
defer walker.deinit();
|
|
|
|
while (try walker.next()) |entry| {
|
|
if (entry.kind != .file) continue;
|
|
if (!std.mem.endsWith(u8, entry.path, ".zig")) continue;
|
|
|
|
// Skip test files - they may legitimately need string comparison for assertions
|
|
if (std.mem.endsWith(u8, entry.path, "_test.zig")) continue;
|
|
if (std.mem.indexOf(u8, entry.path, "test/") != null) continue;
|
|
if (std.mem.startsWith(u8, entry.path, "test")) continue;
|
|
if (std.mem.endsWith(u8, entry.path, "test_runner.zig")) continue;
|
|
|
|
const full_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ path_prefix, entry.path });
|
|
|
|
const file = dir.openFile(entry.path, .{}) catch continue;
|
|
defer file.close();
|
|
|
|
const content = file.readToEndAlloc(allocator, 10 * 1024 * 1024) catch continue;
|
|
defer allocator.free(content);
|
|
|
|
var line_number: usize = 1;
|
|
var line_start: usize = 0;
|
|
|
|
for (content, 0..) |char, i| {
|
|
if (char == '\n') {
|
|
const line = content[line_start..i];
|
|
|
|
const trimmed = std.mem.trim(u8, line, " \t");
|
|
// Skip comments
|
|
if (std.mem.startsWith(u8, trimmed, "//")) {
|
|
line_number += 1;
|
|
line_start = i + 1;
|
|
continue;
|
|
}
|
|
|
|
// Check for std.mem. usage (but allow safe patterns)
|
|
if (std.mem.indexOf(u8, line, "std.mem.")) |idx| {
|
|
const after_match = line[idx + 8 ..];
|
|
|
|
// Allow these safe patterns that don't involve string/byte comparison:
|
|
// - std.mem.Allocator: a type, not a comparison
|
|
// - std.mem.Alignment: a type, not a comparison
|
|
// - std.mem.sort: sorting by custom comparator, not string comparison
|
|
// - std.mem.asBytes: type punning, not string comparison
|
|
// - std.mem.reverse: reversing arrays, not string comparison
|
|
// - std.mem.alignForward: memory alignment arithmetic, not string comparison
|
|
// - std.mem.order: sort ordering (used by sort comparators), not string comparison
|
|
// - std.mem.copyForwards: byte copying, not string comparison
|
|
const is_allowed =
|
|
std.mem.startsWith(u8, after_match, "Allocator") or
|
|
std.mem.startsWith(u8, after_match, "Alignment") or
|
|
std.mem.startsWith(u8, after_match, "sort") or
|
|
std.mem.startsWith(u8, after_match, "asBytes") or
|
|
std.mem.startsWith(u8, after_match, "reverse") or
|
|
std.mem.startsWith(u8, after_match, "alignForward") or
|
|
std.mem.startsWith(u8, after_match, "order") or
|
|
std.mem.startsWith(u8, after_match, "copyForwards");
|
|
|
|
if (!is_allowed) {
|
|
try violations.append(allocator, .{
|
|
.file_path = full_path,
|
|
.line_number = line_number,
|
|
.line_content = try allocator.dupe(u8, trimmed),
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check for findByString usage - should use Ident.Idx comparison instead
|
|
if (std.mem.indexOf(u8, line, "findByString") != null) {
|
|
try violations.append(allocator, .{
|
|
.file_path = full_path,
|
|
.line_number = line_number,
|
|
.line_content = try allocator.dupe(u8, trimmed),
|
|
});
|
|
}
|
|
|
|
// Check for findIdent usage - should use pre-stored Ident.Idx instead
|
|
if (std.mem.indexOf(u8, line, "findIdent") != null) {
|
|
try violations.append(allocator, .{
|
|
.file_path = full_path,
|
|
.line_number = line_number,
|
|
.line_content = try allocator.dupe(u8, trimmed),
|
|
});
|
|
}
|
|
|
|
// Check for getMethodIdent usage - should use pre-stored Ident.Idx instead
|
|
if (std.mem.indexOf(u8, line, "getMethodIdent") != null) {
|
|
try violations.append(allocator, .{
|
|
.file_path = full_path,
|
|
.line_number = line_number,
|
|
.line_content = try allocator.dupe(u8, trimmed),
|
|
});
|
|
}
|
|
|
|
line_number += 1;
|
|
line_start = i + 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/// Build step that checks for @enumFromInt(0) usage in all .zig files.
|
|
///
|
|
/// We forbid @enumFromInt(0) because it hides bugs and makes them harder to debug.
|
|
/// If we need a placeholder value that we believe will never be read, we should
|
|
/// use `undefined` instead - that way our intent is clear, and it can fail in a
|
|
/// more obvious way if our assumption is incorrect.
|
|
const CheckEnumFromIntZeroStep = struct {
|
|
step: Step,
|
|
|
|
fn create(b: *std.Build) *CheckEnumFromIntZeroStep {
|
|
const self = b.allocator.create(CheckEnumFromIntZeroStep) catch @panic("OOM");
|
|
self.* = .{
|
|
.step = Step.init(.{
|
|
.id = Step.Id.custom,
|
|
.name = "check-enum-from-int-zero",
|
|
.owner = b,
|
|
.makeFn = make,
|
|
}),
|
|
};
|
|
return self;
|
|
}
|
|
|
|
fn make(step: *Step, options: Step.MakeOptions) !void {
|
|
_ = options;
|
|
const b = step.owner;
|
|
const allocator = b.allocator;
|
|
|
|
var violations = std.ArrayList(Violation).empty;
|
|
defer violations.deinit(allocator);
|
|
|
|
// Recursively scan src/ for .zig files
|
|
var dir = std.fs.cwd().openDir("src", .{ .iterate = true }) catch |err| {
|
|
return step.fail("Failed to open src directory: {}", .{err});
|
|
};
|
|
defer dir.close();
|
|
|
|
try scanDirectoryForEnumFromIntZero(allocator, dir, "src", &violations);
|
|
|
|
if (violations.items.len > 0) {
|
|
std.debug.print("\n", .{});
|
|
std.debug.print("=" ** 80 ++ "\n", .{});
|
|
std.debug.print("FORBIDDEN PATTERN: @enumFromInt(0)\n", .{});
|
|
std.debug.print("=" ** 80 ++ "\n\n", .{});
|
|
|
|
std.debug.print(
|
|
\\Using @enumFromInt(0) is forbidden in this codebase.
|
|
\\
|
|
\\WHY THIS RULE EXISTS:
|
|
\\ @enumFromInt(0) hides bugs and makes them harder to debug. It creates
|
|
\\ a "valid-looking" value that can silently propagate through the code
|
|
\\ when something goes wrong.
|
|
\\
|
|
\\WHAT TO DO INSTEAD:
|
|
\\ If you need a placeholder value that you believe will never be read,
|
|
\\ use `undefined` instead. This makes your intent clear, and if your
|
|
\\ assumption is wrong and the value IS read, it will fail more obviously.
|
|
\\
|
|
\\ When using `undefined`, add a comment explaining why it's correct there
|
|
\\ (e.g., where it will be overwritten before being read).
|
|
\\
|
|
\\ Example - WRONG:
|
|
\\ .anno = @enumFromInt(0), // placeholder - will be replaced
|
|
\\
|
|
\\ Example - RIGHT:
|
|
\\ .anno = undefined, // overwritten in Phase 1.7 before use
|
|
\\
|
|
\\VIOLATIONS FOUND:
|
|
\\
|
|
, .{});
|
|
|
|
for (violations.items) |violation| {
|
|
std.debug.print(" {s}:{d}: {s}\n", .{
|
|
violation.file_path,
|
|
violation.line_number,
|
|
violation.line_content,
|
|
});
|
|
}
|
|
|
|
std.debug.print("\n" ++ "=" ** 80 ++ "\n", .{});
|
|
|
|
return step.fail(
|
|
"Found {d} uses of @enumFromInt(0). Using placeholder values like this has consistently led to bugs in this code base. " ++
|
|
"Do not use @enumFromInt(0) and also do not uncritically replace it with another placeholder like .first or something like that. " ++
|
|
"If you want it to be uninitialized and are very confident it will be overwritten before it is ever read, then use `undefined`. " ++
|
|
"Otherwise, take a step back and rethink how this code works; there should be a way to implement this in a way that does not use hardcoded placeholder indices like 0! " ++
|
|
"See above for details.",
|
|
.{violations.items.len},
|
|
);
|
|
}
|
|
}
|
|
|
|
const Violation = struct {
|
|
file_path: []const u8,
|
|
line_number: usize,
|
|
line_content: []const u8,
|
|
};
|
|
|
|
fn scanDirectoryForEnumFromIntZero(
|
|
allocator: std.mem.Allocator,
|
|
dir: std.fs.Dir,
|
|
path_prefix: []const u8,
|
|
violations: *std.ArrayList(Violation),
|
|
) !void {
|
|
var walker = try dir.walk(allocator);
|
|
defer walker.deinit();
|
|
|
|
while (try walker.next()) |entry| {
|
|
if (entry.kind != .file) continue;
|
|
if (!std.mem.endsWith(u8, entry.path, ".zig")) continue;
|
|
|
|
const full_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ path_prefix, entry.path });
|
|
|
|
const file = dir.openFile(entry.path, .{}) catch continue;
|
|
defer file.close();
|
|
|
|
const content = file.readToEndAlloc(allocator, 10 * 1024 * 1024) catch continue;
|
|
defer allocator.free(content);
|
|
|
|
var line_number: usize = 1;
|
|
var line_start: usize = 0;
|
|
|
|
for (content, 0..) |char, i| {
|
|
if (char == '\n') {
|
|
const line = content[line_start..i];
|
|
|
|
const trimmed = std.mem.trim(u8, line, " \t");
|
|
// Skip comments
|
|
if (std.mem.startsWith(u8, trimmed, "//")) {
|
|
line_number += 1;
|
|
line_start = i + 1;
|
|
continue;
|
|
}
|
|
|
|
// Check for @enumFromInt(0) usage
|
|
if (std.mem.indexOf(u8, line, "@enumFromInt(0)") != null) {
|
|
try violations.append(allocator, .{
|
|
.file_path = full_path,
|
|
.line_number = line_number,
|
|
.line_content = try allocator.dupe(u8, trimmed),
|
|
});
|
|
}
|
|
|
|
line_number += 1;
|
|
line_start = i + 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/// Build step that checks for unused variable suppression patterns.
|
|
///
|
|
/// In this codebase, we don't use `_ = variable;` to suppress unused variable warnings.
|
|
/// Instead, we delete the unused variable/argument and update all call sites as necessary.
|
|
const CheckUnusedSuppressionStep = struct {
|
|
step: Step,
|
|
|
|
fn create(b: *std.Build) *CheckUnusedSuppressionStep {
|
|
const self = b.allocator.create(CheckUnusedSuppressionStep) catch @panic("OOM");
|
|
self.* = .{
|
|
.step = Step.init(.{
|
|
.id = Step.Id.custom,
|
|
.name = "check-unused-suppression",
|
|
.owner = b,
|
|
.makeFn = make,
|
|
}),
|
|
};
|
|
return self;
|
|
}
|
|
|
|
fn make(step: *Step, _: Step.MakeOptions) !void {
|
|
const b = step.owner;
|
|
const allocator = b.allocator;
|
|
|
|
var violations = std.ArrayList(Violation).empty;
|
|
defer violations.deinit(allocator);
|
|
|
|
// Scan all src/ directories for .zig files
|
|
var dir = std.fs.cwd().openDir("src", .{ .iterate = true }) catch |err| {
|
|
return step.fail("Failed to open src/ directory: {}", .{err});
|
|
};
|
|
defer dir.close();
|
|
|
|
try scanDirectoryForUnusedSuppression(allocator, dir, "src", &violations);
|
|
|
|
if (violations.items.len > 0) {
|
|
std.debug.print("\n", .{});
|
|
std.debug.print("=" ** 80 ++ "\n", .{});
|
|
std.debug.print("UNUSED VARIABLE SUPPRESSION DETECTED\n", .{});
|
|
std.debug.print("=" ** 80 ++ "\n\n", .{});
|
|
|
|
std.debug.print(
|
|
\\In this codebase, we do NOT use `_ = variable;` to suppress unused warnings.
|
|
\\
|
|
\\Instead, you should:
|
|
\\ 1. Delete the unused variable, parameter, or argument
|
|
\\ 2. Update all call sites as necessary
|
|
\\ 3. Propagate the change through the codebase until tests pass
|
|
\\
|
|
\\VIOLATIONS FOUND:
|
|
\\
|
|
, .{});
|
|
|
|
for (violations.items) |violation| {
|
|
std.debug.print(" {s}:{d}: {s}\n", .{
|
|
violation.file_path,
|
|
violation.line_number,
|
|
violation.line_content,
|
|
});
|
|
}
|
|
|
|
std.debug.print("\n" ++ "=" ** 80 ++ "\n", .{});
|
|
|
|
return step.fail(
|
|
"Found {d} unused variable suppression patterns (`_ = identifier;`). " ++
|
|
"Delete the unused variables and update call sites instead.",
|
|
.{violations.items.len},
|
|
);
|
|
}
|
|
}
|
|
|
|
const Violation = struct {
|
|
file_path: []const u8,
|
|
line_number: usize,
|
|
line_content: []const u8,
|
|
};
|
|
|
|
fn scanDirectoryForUnusedSuppression(
|
|
allocator: std.mem.Allocator,
|
|
dir: std.fs.Dir,
|
|
path_prefix: []const u8,
|
|
violations: *std.ArrayList(Violation),
|
|
) !void {
|
|
var walker = try dir.walk(allocator);
|
|
defer walker.deinit();
|
|
|
|
while (try walker.next()) |entry| {
|
|
if (entry.kind != .file) continue;
|
|
if (!std.mem.endsWith(u8, entry.path, ".zig")) continue;
|
|
|
|
const full_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ path_prefix, entry.path });
|
|
|
|
const file = dir.openFile(entry.path, .{}) catch continue;
|
|
defer file.close();
|
|
|
|
const content = file.readToEndAlloc(allocator, 10 * 1024 * 1024) catch continue;
|
|
defer allocator.free(content);
|
|
|
|
var line_number: usize = 1;
|
|
var line_start: usize = 0;
|
|
|
|
for (content, 0..) |char, i| {
|
|
if (char == '\n') {
|
|
const line = content[line_start..i];
|
|
const trimmed = std.mem.trim(u8, line, " \t");
|
|
|
|
// Check for pattern: _ = identifier;
|
|
// where identifier is alphanumeric with underscores
|
|
if (isUnusedSuppression(trimmed)) {
|
|
try violations.append(allocator, .{
|
|
.file_path = full_path,
|
|
.line_number = line_number,
|
|
.line_content = try allocator.dupe(u8, trimmed),
|
|
});
|
|
}
|
|
|
|
line_number += 1;
|
|
line_start = i + 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn isUnusedSuppression(line: []const u8) bool {
|
|
// Pattern: `_ = identifier;` where identifier is alphanumeric with underscores
|
|
// Must start with "_ = " and end with ";"
|
|
if (!std.mem.startsWith(u8, line, "_ = ")) return false;
|
|
if (!std.mem.endsWith(u8, line, ";")) return false;
|
|
|
|
// Extract the identifier part (between "_ = " and ";")
|
|
const identifier = line[4 .. line.len - 1];
|
|
|
|
// Must have at least one character
|
|
if (identifier.len == 0) return false;
|
|
|
|
// Check that identifier contains only alphanumeric chars and underscores
|
|
// Also allow dots for field access like `_ = self.field;` which we also want to catch
|
|
for (identifier) |c| {
|
|
if (!std.ascii.isAlphanumeric(c) and c != '_' and c != '.') {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
};
|
|
|
|
/// Build step that checks for @panic and std.debug.panic usage in interpreter and builtins.
|
|
///
|
|
/// In Roc's design philosophy, compile-time errors become runtime errors with helpful messages.
|
|
/// Users can run apps despite errors, and we provide actionable feedback. Using @panic unwinds
|
|
/// the stack and prevents showing helpful error messages.
|
|
///
|
|
/// Additionally, in WASM builds, @panic compiles to the `unreachable` instruction with no
|
|
/// message output, making debugging impossible. All runtime code must use roc_ops.crash()
|
|
/// to ensure error messages are properly displayed.
|
|
const CheckPanicStep = struct {
|
|
step: Step,
|
|
|
|
// Files to scan individually
|
|
const scan_files = [_][]const u8{
|
|
"src/eval/interpreter.zig",
|
|
"src/eval/StackValue.zig",
|
|
};
|
|
|
|
// Directories to scan (all .zig files within)
|
|
const scan_dirs = [_][]const u8{
|
|
"src/builtins",
|
|
};
|
|
|
|
// Files to exclude from scanning (test-only files)
|
|
const excluded_files = [_][]const u8{
|
|
"fuzz_sort.zig",
|
|
};
|
|
|
|
// Line-level allowlist patterns - if any of these appear on the line, allow the @panic
|
|
const allowlist_patterns = [_][]const u8{
|
|
"trace_modules", // traceDbg helper in interpreter
|
|
};
|
|
|
|
// File-specific line ranges to exclude (test-only code)
|
|
// Format: { file_suffix, start_line, end_line }
|
|
const ExcludedRange = struct { file: []const u8, start: usize, end: usize };
|
|
const excluded_ranges = [_]ExcludedRange{
|
|
// TestEnv struct in utils.zig is test-only (lines 60-214)
|
|
.{ .file = "utils.zig", .start = 60, .end = 214 },
|
|
};
|
|
|
|
fn create(b: *std.Build) *CheckPanicStep {
|
|
const self = b.allocator.create(CheckPanicStep) catch @panic("OOM");
|
|
self.* = .{
|
|
.step = Step.init(.{
|
|
.id = Step.Id.custom,
|
|
.name = "check-panic-usage",
|
|
.owner = b,
|
|
.makeFn = makePanic,
|
|
}),
|
|
};
|
|
return self;
|
|
}
|
|
|
|
fn isExcludedFile(file_name: []const u8) bool {
|
|
for (excluded_files) |excluded| {
|
|
if (std.mem.eql(u8, file_name, excluded)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
fn isAllowlisted(line: []const u8) bool {
|
|
for (allowlist_patterns) |pattern| {
|
|
if (std.mem.indexOf(u8, line, pattern) != null) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
fn isInExcludedRange(file_path: []const u8, line_number: usize) bool {
|
|
for (excluded_ranges) |range| {
|
|
if (std.mem.endsWith(u8, file_path, range.file)) {
|
|
if (line_number >= range.start and line_number <= range.end) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
fn scanFile(allocator: std.mem.Allocator, file_path: []const u8, violations: *std.ArrayList(Violation)) !void {
|
|
const file = std.fs.cwd().openFile(file_path, .{}) catch |err| {
|
|
std.debug.print("Warning: Failed to open {s}: {}\n", .{ file_path, err });
|
|
return;
|
|
};
|
|
defer file.close();
|
|
|
|
const content = file.readToEndAlloc(allocator, 50 * 1024 * 1024) catch |err| {
|
|
std.debug.print("Warning: Failed to read {s}: {}\n", .{ file_path, err });
|
|
return;
|
|
};
|
|
defer allocator.free(content);
|
|
|
|
var line_number: usize = 1;
|
|
var line_start: usize = 0;
|
|
|
|
for (content, 0..) |char, i| {
|
|
if (char == '\n') {
|
|
const line = content[line_start..i];
|
|
const trimmed = std.mem.trim(u8, line, " \t");
|
|
|
|
// Skip comments
|
|
if (!std.mem.startsWith(u8, trimmed, "//")) {
|
|
// Check for @panic usage
|
|
const has_panic = std.mem.indexOf(u8, line, "@panic(") != null;
|
|
// Check for std.debug.panic usage
|
|
const has_debug_panic = std.mem.indexOf(u8, line, "std.debug.panic") != null;
|
|
|
|
if (has_panic or has_debug_panic) {
|
|
if (!isAllowlisted(line) and !isInExcludedRange(file_path, line_number)) {
|
|
try violations.append(allocator, .{
|
|
.file_path = try allocator.dupe(u8, file_path),
|
|
.line_number = line_number,
|
|
.line_content = try allocator.dupe(u8, trimmed),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
line_number += 1;
|
|
line_start = i + 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn makePanic(step: *Step, _: Step.MakeOptions) !void {
|
|
const b = step.owner;
|
|
const allocator = b.allocator;
|
|
|
|
var violations = std.ArrayList(Violation).empty;
|
|
defer violations.deinit(allocator);
|
|
|
|
// Scan individual files
|
|
for (scan_files) |file_path| {
|
|
try scanFile(allocator, file_path, &violations);
|
|
}
|
|
|
|
// Scan directories
|
|
for (scan_dirs) |dir_path| {
|
|
var dir = std.fs.cwd().openDir(dir_path, .{ .iterate = true }) catch |err| {
|
|
std.debug.print("Warning: Failed to open directory {s}: {}\n", .{ dir_path, err });
|
|
continue;
|
|
};
|
|
defer dir.close();
|
|
|
|
var iter = dir.iterate();
|
|
while (try iter.next()) |entry| {
|
|
if (entry.kind == .file and std.mem.endsWith(u8, entry.name, ".zig")) {
|
|
if (!isExcludedFile(entry.name)) {
|
|
const full_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ dir_path, entry.name });
|
|
defer allocator.free(full_path);
|
|
try scanFile(allocator, full_path, &violations);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (violations.items.len > 0) {
|
|
std.debug.print("\n", .{});
|
|
std.debug.print("=" ** 80 ++ "\n", .{});
|
|
std.debug.print("FORBIDDEN PATTERN: @panic / std.debug.panic in runtime code\n", .{});
|
|
std.debug.print("=" ** 80 ++ "\n\n", .{});
|
|
|
|
std.debug.print(
|
|
\\Using @panic or std.debug.panic is forbidden in interpreter and builtins.
|
|
\\
|
|
\\WHY THIS RULE EXISTS:
|
|
\\ 1. Roc's design philosophy is that compile-time errors become runtime errors with
|
|
\\ helpful messages. Users can run apps despite errors, and we provide actionable
|
|
\\ feedback. @panic unwinds the stack and prevents us from showing helpful errors.
|
|
\\
|
|
\\ 2. In WASM builds, @panic compiles to the `unreachable` instruction with NO
|
|
\\ message output, making debugging impossible.
|
|
\\
|
|
\\WHAT TO DO INSTEAD:
|
|
\\ In interpreter.zig, use the triggerCrash() method:
|
|
\\
|
|
\\ self.triggerCrash("Description of the error", false, roc_ops);
|
|
\\
|
|
\\ In StackValue.zig and builtins, use roc_ops.crash():
|
|
\\
|
|
\\ roc_ops.crash("Description of the error");
|
|
\\
|
|
\\ For debug output, use roc_ops.dbg():
|
|
\\
|
|
\\ roc_ops.dbg("Debug message");
|
|
\\
|
|
\\VIOLATIONS FOUND:
|
|
\\
|
|
, .{});
|
|
|
|
for (violations.items) |violation| {
|
|
std.debug.print(" {s}:{d}: {s}\n", .{
|
|
violation.file_path,
|
|
violation.line_number,
|
|
violation.line_content,
|
|
});
|
|
}
|
|
|
|
std.debug.print("\n" ++ "=" ** 80 ++ "\n", .{});
|
|
|
|
return step.fail(
|
|
"Found {d} uses of @panic or std.debug.panic in runtime code. " ++
|
|
"Use roc_ops.crash() to report errors through the proper RocOps crash handler. " ++
|
|
"See above for details.",
|
|
.{violations.items.len},
|
|
);
|
|
}
|
|
}
|
|
|
|
const Violation = struct {
|
|
file_path: []const u8,
|
|
line_number: usize,
|
|
line_content: []const u8,
|
|
};
|
|
};
|
|
|
|
/// Build step that checks for global stdio usage in CLI code.
|
|
///
|
|
/// The CLI code uses a context-based I/O pattern where stdout/stderr are accessed
|
|
/// through `ctx.io.stdout()` and `ctx.io.stderr()`. This prepares for Zig's upcoming
|
|
/// I/O interface changes where I/O is passed through functions (like Allocator).
|
|
///
|
|
/// This step enforces that pattern by failing the build if direct global stdio
|
|
/// access is found in src/cli/main.zig.
|
|
const CheckCliGlobalStdioStep = struct {
|
|
step: Step,
|
|
|
|
fn create(b: *std.Build) *CheckCliGlobalStdioStep {
|
|
const self = b.allocator.create(CheckCliGlobalStdioStep) catch @panic("OOM");
|
|
self.* = .{
|
|
.step = Step.init(.{
|
|
.id = Step.Id.custom,
|
|
.name = "check-cli-global-stdio",
|
|
.owner = b,
|
|
.makeFn = make,
|
|
}),
|
|
};
|
|
return self;
|
|
}
|
|
|
|
fn make(step: *Step, _: Step.MakeOptions) !void {
|
|
const b = step.owner;
|
|
const allocator = b.allocator;
|
|
|
|
var violations = std.ArrayList(Violation).empty;
|
|
defer violations.deinit(allocator);
|
|
|
|
// Only scan src/cli/main.zig
|
|
const file_path = "src/cli/main.zig";
|
|
const file = std.fs.cwd().openFile(file_path, .{}) catch |err| {
|
|
return step.fail("Failed to open {s}: {}", .{ file_path, err });
|
|
};
|
|
defer file.close();
|
|
|
|
const content = file.readToEndAlloc(allocator, 10 * 1024 * 1024) catch |err| {
|
|
return step.fail("Failed to read {s}: {}", .{ file_path, err });
|
|
};
|
|
defer allocator.free(content);
|
|
|
|
var line_number: usize = 1;
|
|
var line_start: usize = 0;
|
|
|
|
for (content, 0..) |char, i| {
|
|
if (char == '\n') {
|
|
const line = content[line_start..i];
|
|
const trimmed = std.mem.trim(u8, line, " \t");
|
|
|
|
// Check for forbidden patterns that indicate global stdio usage
|
|
// These patterns bypass ctx.io and use global state
|
|
const forbidden_patterns = [_][]const u8{
|
|
"std.io.getStdOut()",
|
|
"std.io.getStdErr()",
|
|
"std.fs.File.stdout()",
|
|
"std.fs.File.stderr()",
|
|
};
|
|
|
|
for (forbidden_patterns) |pattern| {
|
|
if (std.mem.indexOf(u8, trimmed, pattern) != null) {
|
|
try violations.append(allocator, .{
|
|
.file_path = file_path,
|
|
.line_number = line_number,
|
|
.line_content = try allocator.dupe(u8, trimmed),
|
|
.pattern = pattern,
|
|
});
|
|
}
|
|
}
|
|
|
|
line_number += 1;
|
|
line_start = i + 1;
|
|
}
|
|
}
|
|
|
|
if (violations.items.len > 0) {
|
|
std.debug.print("\n", .{});
|
|
std.debug.print("=" ** 80 ++ "\n", .{});
|
|
std.debug.print("GLOBAL STDIO USAGE DETECTED IN CLI\n", .{});
|
|
std.debug.print("=" ** 80 ++ "\n\n", .{});
|
|
|
|
std.debug.print(
|
|
\\In the CLI code, we use context-based I/O, not global stdio functions.
|
|
\\
|
|
\\WHY THIS RULE EXISTS:
|
|
\\ 1. TESTABILITY: Context-based I/O allows tests to inject mock writers
|
|
\\ to capture and verify output.
|
|
\\
|
|
\\ 2. FUTURE COMPATIBILITY: Zig's upcoming I/O interface will pass I/O
|
|
\\ through functions (like Allocator). Using ctx.io prepares us for this.
|
|
\\
|
|
\\ 3. CONSISTENCY: All CLI functions receive ctx which contains allocators
|
|
\\ and I/O. This provides a uniform interface for resources.
|
|
\\
|
|
\\WHAT TO DO INSTEAD:
|
|
\\ Access stdout/stderr through the CliContext:
|
|
\\
|
|
\\ Example - WRONG:
|
|
\\ const stdout = std.io.getStdOut().writer();
|
|
\\ const stderr = std.fs.File.stderr().writer();
|
|
\\
|
|
\\ Example - RIGHT:
|
|
\\ const stdout = ctx.io.stdout();
|
|
\\ const stderr = ctx.io.stderr();
|
|
\\
|
|
\\VIOLATIONS FOUND:
|
|
\\
|
|
, .{});
|
|
|
|
for (violations.items) |violation| {
|
|
std.debug.print(" {s}:{d}: found `{s}` in: {s}\n", .{
|
|
violation.file_path,
|
|
violation.line_number,
|
|
violation.pattern,
|
|
violation.line_content,
|
|
});
|
|
}
|
|
|
|
std.debug.print("\n" ++ "=" ** 80 ++ "\n", .{});
|
|
|
|
return step.fail(
|
|
"Found {d} global stdio usage(s) in CLI code. " ++
|
|
"Use ctx.io.stdout() and ctx.io.stderr() instead.",
|
|
.{violations.items.len},
|
|
);
|
|
}
|
|
}
|
|
|
|
const Violation = struct {
|
|
file_path: []const u8,
|
|
line_number: usize,
|
|
line_content: []const u8,
|
|
pattern: []const u8,
|
|
};
|
|
};
|
|
|
|
fn checkFxPlatformTestCoverage(step: *Step) !void {
|
|
const b = step.owner;
|
|
std.debug.print("---- checking fx platform test coverage ----\n", .{});
|
|
|
|
const allocator = b.allocator;
|
|
|
|
// Get all .roc files in test/fx (excluding subdirectories)
|
|
var fx_dir = try std.fs.cwd().openDir("test/fx", .{ .iterate = true });
|
|
defer fx_dir.close();
|
|
|
|
var roc_files = std.ArrayList([]const u8).empty;
|
|
defer {
|
|
for (roc_files.items) |file| {
|
|
allocator.free(file);
|
|
}
|
|
roc_files.deinit(allocator);
|
|
}
|
|
|
|
var dir_iter = fx_dir.iterate();
|
|
while (try dir_iter.next()) |entry| {
|
|
if (entry.kind == .file and std.mem.endsWith(u8, entry.name, ".roc")) {
|
|
const file_name = try allocator.dupe(u8, entry.name);
|
|
try roc_files.append(allocator, file_name);
|
|
}
|
|
}
|
|
|
|
// Sort the list for consistent output
|
|
std.mem.sort([]const u8, roc_files.items, {}, struct {
|
|
fn lessThan(_: void, lhs: []const u8, rhs: []const u8) bool {
|
|
return std.mem.order(u8, lhs, rhs) == .lt;
|
|
}
|
|
}.lessThan);
|
|
|
|
// Find all references to test/fx/*.roc files in test source files
|
|
var tested_files = std.StringHashMap(void).init(allocator);
|
|
defer {
|
|
var key_iter = tested_files.keyIterator();
|
|
while (key_iter.next()) |key| {
|
|
allocator.free(key.*);
|
|
}
|
|
tested_files.deinit();
|
|
}
|
|
|
|
// Scan both the test file and the shared specs file
|
|
const test_files_to_scan = [_][]const u8{
|
|
"src/cli/test/fx_platform_test.zig",
|
|
"src/cli/test/fx_test_specs.zig",
|
|
};
|
|
|
|
for (test_files_to_scan) |test_file_path| {
|
|
const test_file_contents = std.fs.cwd().readFileAlloc(allocator, test_file_path, 1024 * 1024) catch |err| {
|
|
std.debug.print("Warning: Could not read {s}: {}\n", .{ test_file_path, err });
|
|
continue;
|
|
};
|
|
defer allocator.free(test_file_contents);
|
|
|
|
var line_iter = std.mem.splitScalar(u8, test_file_contents, '\n');
|
|
while (line_iter.next()) |line| {
|
|
// Look for patterns like "test/fx/filename.roc"
|
|
var search_start: usize = 0;
|
|
while (std.mem.indexOfPos(u8, line, search_start, "test/fx/")) |idx| {
|
|
const rest_of_line = line[idx..];
|
|
// Find the end of the filename
|
|
if (std.mem.indexOf(u8, rest_of_line, ".roc")) |roc_pos| {
|
|
const full_path = rest_of_line[0 .. roc_pos + 4]; // Include ".roc"
|
|
// Extract just the filename (after "test/fx/")
|
|
const filename = full_path["test/fx/".len..];
|
|
// Only count files in test/fx (not subdirectories like test/fx/subdir/)
|
|
if (std.mem.indexOf(u8, filename, "/") == null) {
|
|
// Dupe the filename since the source buffer will be freed
|
|
const duped_filename = try allocator.dupe(u8, filename);
|
|
try tested_files.put(duped_filename, {});
|
|
}
|
|
}
|
|
search_start = idx + 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find missing tests
|
|
var missing_tests = std.ArrayList([]const u8).empty;
|
|
defer missing_tests.deinit(allocator);
|
|
|
|
for (roc_files.items) |roc_file| {
|
|
if (!tested_files.contains(roc_file)) {
|
|
try missing_tests.append(allocator, roc_file);
|
|
}
|
|
}
|
|
|
|
// Report results
|
|
if (missing_tests.items.len > 0) {
|
|
std.debug.print("\nERROR: The following .roc files in test/fx/ do not have tests:\n", .{});
|
|
for (missing_tests.items) |missing_file| {
|
|
std.debug.print(" - {s}\n", .{missing_file});
|
|
}
|
|
std.debug.print("\nPlease add tests in fx_platform_test.zig or fx_test_specs.zig, or remove these files from test/fx/.\n", .{});
|
|
return step.fail("{d} .roc file(s) in test/fx/ are missing tests", .{missing_tests.items.len});
|
|
}
|
|
|
|
std.debug.print("All {d} .roc files in test/fx/ have tests.\n", .{roc_files.items.len});
|
|
}
|
|
|
|
const CheckFxStep = struct {
|
|
step: Step,
|
|
|
|
fn create(b: *std.Build) *CheckFxStep {
|
|
const self = b.allocator.create(CheckFxStep) catch @panic("OOM");
|
|
self.* = .{
|
|
.step = Step.init(.{
|
|
.id = Step.Id.custom,
|
|
.name = "checkfx-inner",
|
|
.owner = b,
|
|
.makeFn = make,
|
|
}),
|
|
};
|
|
return self;
|
|
}
|
|
|
|
fn make(step: *Step, options: Step.MakeOptions) !void {
|
|
_ = options;
|
|
try checkFxPlatformTestCoverage(step);
|
|
}
|
|
};
|
|
|
|
const MiniCiStep = struct {
|
|
step: Step,
|
|
|
|
fn create(b: *std.Build) *MiniCiStep {
|
|
const self = b.allocator.create(MiniCiStep) catch @panic("OOM");
|
|
self.* = .{
|
|
.step = Step.init(.{
|
|
.id = Step.Id.custom,
|
|
.name = "minici-inner",
|
|
.owner = b,
|
|
.makeFn = make,
|
|
}),
|
|
};
|
|
return self;
|
|
}
|
|
|
|
fn make(step: *Step, options: Step.MakeOptions) !void {
|
|
_ = options;
|
|
|
|
// Run the sequence of `zig build` commands that make up the
|
|
// mini CI pipeline.
|
|
try runSubBuild(step, "fmt", "zig build fmt");
|
|
try runZigLints(step);
|
|
try checkTestWiring(step);
|
|
try runSubBuild(step, null, "zig build");
|
|
try checkBuiltinRocFormatting(step);
|
|
try runSubBuild(step, "snapshot", "zig build snapshot");
|
|
try checkSnapshotChanges(step);
|
|
try checkFxPlatformTestCoverage(step);
|
|
try runSubBuild(step, "test", "zig build test");
|
|
try runSubBuild(step, "test-playground", "zig build test-playground");
|
|
try runSubBuild(step, "test-serialization-sizes", "zig build test-serialization-sizes");
|
|
try runSubBuild(step, "test-cli", "zig build test-cli");
|
|
}
|
|
|
|
fn runZigLints(step: *Step) !void {
|
|
const b = step.owner;
|
|
std.debug.print("---- minici: running zig lints ----\n", .{});
|
|
|
|
var child_argv = std.ArrayList([]const u8).empty;
|
|
defer child_argv.deinit(b.allocator);
|
|
|
|
try child_argv.append(b.allocator, b.graph.zig_exe);
|
|
try child_argv.append(b.allocator, "run");
|
|
try child_argv.append(b.allocator, "ci/zig_lints.zig");
|
|
|
|
var child = std.process.Child.init(child_argv.items, b.allocator);
|
|
child.stdin_behavior = .Inherit;
|
|
child.stdout_behavior = .Inherit;
|
|
child.stderr_behavior = .Inherit;
|
|
|
|
const term = try child.spawnAndWait();
|
|
|
|
switch (term) {
|
|
.Exited => |code| {
|
|
if (code != 0) {
|
|
return step.fail("Zig lints failed. Run 'zig run ci/zig_lints.zig' to see details.", .{});
|
|
}
|
|
},
|
|
else => {
|
|
return step.fail("zig run ci/zig_lints.zig terminated abnormally", .{});
|
|
},
|
|
}
|
|
}
|
|
|
|
fn checkBuiltinRocFormatting(step: *Step) !void {
|
|
const b = step.owner;
|
|
std.debug.print("---- minici: checking Builtin.roc formatting ----\n", .{});
|
|
|
|
var child_argv = std.ArrayList([]const u8).empty;
|
|
defer child_argv.deinit(b.allocator);
|
|
|
|
try child_argv.append(b.allocator, "./zig-out/bin/roc");
|
|
try child_argv.append(b.allocator, "fmt");
|
|
try child_argv.append(b.allocator, "--check");
|
|
try child_argv.append(b.allocator, "src/build/roc/Builtin.roc");
|
|
|
|
var child = std.process.Child.init(child_argv.items, b.allocator);
|
|
child.stdin_behavior = .Inherit;
|
|
child.stdout_behavior = .Inherit;
|
|
child.stderr_behavior = .Inherit;
|
|
|
|
const term = try child.spawnAndWait();
|
|
|
|
switch (term) {
|
|
.Exited => |code| {
|
|
if (code != 0) {
|
|
return step.fail(
|
|
"src/build/roc/Builtin.roc is not formatted. " ++
|
|
"Run 'zig build run -- fmt src/build/roc/Builtin.roc' to format it.",
|
|
.{},
|
|
);
|
|
}
|
|
},
|
|
else => {
|
|
return step.fail("roc fmt --check terminated abnormally", .{});
|
|
},
|
|
}
|
|
}
|
|
|
|
fn checkSnapshotChanges(step: *Step) !void {
|
|
const b = step.owner;
|
|
std.debug.print("---- minici: checking for snapshot changes ----\n", .{});
|
|
|
|
var child_argv = std.ArrayList([]const u8).empty;
|
|
defer child_argv.deinit(b.allocator);
|
|
|
|
try child_argv.append(b.allocator, "git");
|
|
try child_argv.append(b.allocator, "diff");
|
|
try child_argv.append(b.allocator, "--exit-code");
|
|
try child_argv.append(b.allocator, "test/snapshots");
|
|
|
|
var child = std.process.Child.init(child_argv.items, b.allocator);
|
|
child.stdin_behavior = .Inherit;
|
|
child.stdout_behavior = .Inherit;
|
|
child.stderr_behavior = .Inherit;
|
|
|
|
const term = try child.spawnAndWait();
|
|
|
|
switch (term) {
|
|
.Exited => |code| {
|
|
if (code != 0) {
|
|
return step.fail(
|
|
"Snapshots in 'test/snapshots' have changed. " ++
|
|
"Run 'zig build snapshot' locally, review the updates, and commit the changes.",
|
|
.{},
|
|
);
|
|
}
|
|
},
|
|
else => {
|
|
return step.fail("git diff terminated abnormally", .{});
|
|
},
|
|
}
|
|
}
|
|
|
|
fn runSubBuild(
|
|
step: *Step,
|
|
step_name: ?[]const u8,
|
|
display: []const u8,
|
|
) !void {
|
|
const b = step.owner;
|
|
std.debug.print("---- minici: running `{s}` ----\n", .{display});
|
|
|
|
var child_argv = std.ArrayList([]const u8).empty;
|
|
defer child_argv.deinit(b.allocator);
|
|
|
|
// Build a clean zig build command for the requested step.
|
|
try child_argv.append(b.allocator, b.graph.zig_exe); // zig executable
|
|
try child_argv.append(b.allocator, "build");
|
|
|
|
if (step_name) |name| {
|
|
try child_argv.append(b.allocator, name);
|
|
}
|
|
|
|
var child = std.process.Child.init(child_argv.items, b.allocator);
|
|
child.stdin_behavior = .Inherit;
|
|
child.stdout_behavior = .Inherit;
|
|
child.stderr_behavior = .Inherit;
|
|
|
|
const term = try child.spawnAndWait();
|
|
|
|
switch (term) {
|
|
.Exited => |code| {
|
|
if (code != 0) {
|
|
return step.fail("`{s}` failed with exit code {d}", .{ display, code });
|
|
}
|
|
},
|
|
else => {
|
|
return step.fail("`{s}` terminated abnormally", .{display});
|
|
},
|
|
}
|
|
}
|
|
|
|
fn checkTestWiring(step: *Step) !void {
|
|
const b = step.owner;
|
|
std.debug.print("---- minici: checking test wiring ----\n", .{});
|
|
|
|
var child_argv = std.ArrayList([]const u8).empty;
|
|
defer child_argv.deinit(b.allocator);
|
|
|
|
try child_argv.append(b.allocator, b.graph.zig_exe);
|
|
try child_argv.append(b.allocator, "run");
|
|
try child_argv.append(b.allocator, "ci/check_test_wiring.zig");
|
|
|
|
var child = std.process.Child.init(child_argv.items, b.allocator);
|
|
child.stdin_behavior = .Inherit;
|
|
child.stdout_behavior = .Inherit;
|
|
child.stderr_behavior = .Inherit;
|
|
|
|
const term = try child.spawnAndWait();
|
|
|
|
switch (term) {
|
|
.Exited => |code| {
|
|
if (code != 0) {
|
|
return step.fail(
|
|
"Test wiring check failed. Run 'zig run ci/check_test_wiring.zig' to see details.",
|
|
.{},
|
|
);
|
|
}
|
|
},
|
|
else => {
|
|
return step.fail("zig run ci/check_test_wiring.zig terminated abnormally", .{});
|
|
},
|
|
}
|
|
}
|
|
};
|
|
|
|
fn createAndRunBuiltinCompiler(
|
|
b: *std.Build,
|
|
roc_modules: modules.RocModules,
|
|
flag_enable_tracy: ?[]const u8,
|
|
roc_files: []const []const u8,
|
|
) *Step.Run {
|
|
// Build and run the compiler
|
|
const builtin_compiler_exe = b.addExecutable(.{
|
|
.name = "builtin_compiler",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("src/build/builtin_compiler/main.zig"),
|
|
.target = b.graph.host, // this runs at build time on the *host* machine!
|
|
.optimize = .Debug, // No need to optimize - only compiles builtin modules
|
|
// Note: libc linking is handled by add_tracy below (required when tracy is enabled)
|
|
}),
|
|
});
|
|
configureBackend(builtin_compiler_exe, b.graph.host);
|
|
|
|
// Add only the minimal modules needed for parsing/checking
|
|
builtin_compiler_exe.root_module.addImport("base", roc_modules.base);
|
|
builtin_compiler_exe.root_module.addImport("collections", roc_modules.collections);
|
|
builtin_compiler_exe.root_module.addImport("types", roc_modules.types);
|
|
builtin_compiler_exe.root_module.addImport("parse", roc_modules.parse);
|
|
builtin_compiler_exe.root_module.addImport("can", roc_modules.can);
|
|
builtin_compiler_exe.root_module.addImport("check", roc_modules.check);
|
|
builtin_compiler_exe.root_module.addImport("reporting", roc_modules.reporting);
|
|
builtin_compiler_exe.root_module.addImport("builtins", roc_modules.builtins);
|
|
|
|
// Add tracy support (required by parse/can/check modules)
|
|
add_tracy(b, roc_modules.build_options, builtin_compiler_exe, b.graph.host, false, flag_enable_tracy);
|
|
|
|
// Run the builtin compiler to generate .bin files in zig-out/builtins/
|
|
const run_builtin_compiler = b.addRunArtifact(builtin_compiler_exe);
|
|
|
|
// Add all .roc files as explicit file inputs so Zig's cache tracks them
|
|
for (roc_files) |roc_path| {
|
|
run_builtin_compiler.addFileArg(b.path(roc_path));
|
|
}
|
|
|
|
return run_builtin_compiler;
|
|
}
|
|
|
|
fn createTestPlatformHostLib(
|
|
b: *std.Build,
|
|
name: []const u8,
|
|
host_path: []const u8,
|
|
target: ResolvedTarget,
|
|
optimize: OptimizeMode,
|
|
roc_modules: modules.RocModules,
|
|
strip: bool,
|
|
omit_frame_pointer: ?bool,
|
|
) *Step.Compile {
|
|
const lib = b.addLibrary(.{
|
|
.name = name,
|
|
.linkage = .static,
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path(host_path),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
.strip = strip,
|
|
.omit_frame_pointer = omit_frame_pointer,
|
|
.pic = true, // Enable Position Independent Code for PIE compatibility
|
|
}),
|
|
});
|
|
configureBackend(lib, target);
|
|
lib.root_module.addImport("builtins", roc_modules.builtins);
|
|
lib.root_module.addImport("build_options", roc_modules.build_options);
|
|
// Don't bundle compiler-rt in host libraries - roc_shim provides it
|
|
// Bundling it here causes duplicate symbol errors on Windows
|
|
lib.bundle_compiler_rt = false;
|
|
|
|
return lib;
|
|
}
|
|
|
|
/// Builds a test platform host library and sets up a step to copy it to the target-specific directory.
|
|
/// Returns the final step for dependency wiring.
|
|
fn buildAndCopyTestPlatformHostLib(
|
|
b: *std.Build,
|
|
platform_dir: []const u8,
|
|
target: ResolvedTarget,
|
|
target_name: []const u8,
|
|
optimize: OptimizeMode,
|
|
roc_modules: modules.RocModules,
|
|
strip: bool,
|
|
omit_frame_pointer: ?bool,
|
|
) *Step {
|
|
const lib = createTestPlatformHostLib(
|
|
b,
|
|
b.fmt("test_platform_{s}_host_{s}", .{ platform_dir, target_name }),
|
|
b.pathJoin(&.{ "test", platform_dir, "platform/host.zig" }),
|
|
target,
|
|
optimize,
|
|
roc_modules,
|
|
strip,
|
|
omit_frame_pointer,
|
|
);
|
|
|
|
// Use correct filename for target platform
|
|
const host_filename = if (target.result.os.tag == .windows) "host.lib" else "libhost.a";
|
|
const archive_path = b.pathJoin(&.{ "test", platform_dir, "platform/targets", target_name, host_filename });
|
|
|
|
const copy_step = b.addUpdateSourceFiles();
|
|
copy_step.addCopyFileToSource(lib.getEmittedBin(), archive_path);
|
|
|
|
// Workaround for Zig bug https://codeberg.org/ziglang/zig/issues/30572
|
|
// Zig's archive generator doesn't add the required padding byte after odd-sized
|
|
// members, causing lld to reject the archive with:
|
|
// "Archive::children failed: truncated or malformed archive"
|
|
if (target.result.os.tag != .windows) {
|
|
const fix_step = FixArchivePaddingStep.create(b, archive_path);
|
|
fix_step.step.dependOn(©_step.step);
|
|
return &fix_step.step;
|
|
}
|
|
|
|
return ©_step.step;
|
|
}
|
|
|
|
// Workaround for Zig bug https://codeberg.org/ziglang/zig/issues/30572
|
|
const FixArchivePaddingStep = struct {
|
|
step: Step,
|
|
archive_path: []const u8,
|
|
|
|
fn create(b: *std.Build, archive_path: []const u8) *FixArchivePaddingStep {
|
|
const self = b.allocator.create(FixArchivePaddingStep) catch @panic("OOM");
|
|
self.* = .{
|
|
.step = Step.init(.{
|
|
.id = Step.Id.custom,
|
|
.name = "fix-archive-padding",
|
|
.owner = b,
|
|
.makeFn = make,
|
|
}),
|
|
.archive_path = archive_path,
|
|
};
|
|
return self;
|
|
}
|
|
|
|
fn make(step: *Step, options: Step.MakeOptions) !void {
|
|
_ = options;
|
|
const self: *FixArchivePaddingStep = @fieldParentPtr("step", step);
|
|
|
|
const file = std.fs.cwd().openFile(self.archive_path, .{ .mode = .read_write }) catch |err| {
|
|
std.debug.print("Warning: Could not open archive {s}: {s}\n", .{ self.archive_path, @errorName(err) });
|
|
return;
|
|
};
|
|
defer file.close();
|
|
|
|
const stat = try file.stat();
|
|
const file_size = stat.size;
|
|
|
|
// AR format requires archives to end on an even byte boundary.
|
|
// If file size is odd, append a newline padding byte.
|
|
if (file_size % 2 == 1) {
|
|
try file.seekTo(file_size);
|
|
try file.writeAll("\n");
|
|
}
|
|
}
|
|
};
|
|
|
|
/// Custom build step that clears the Roc cache directory.
|
|
/// Uses Zig's native filesystem APIs for cross-platform support.
|
|
const ClearRocCacheStep = struct {
|
|
step: Step,
|
|
|
|
fn create(b: *std.Build) *ClearRocCacheStep {
|
|
const self = b.allocator.create(ClearRocCacheStep) catch @panic("OOM");
|
|
self.* = .{
|
|
.step = Step.init(.{
|
|
.id = Step.Id.custom,
|
|
.name = "clear-roc-cache",
|
|
.owner = b,
|
|
.makeFn = make,
|
|
}),
|
|
};
|
|
return self;
|
|
}
|
|
|
|
fn make(step: *Step, options: Step.MakeOptions) !void {
|
|
_ = options;
|
|
|
|
const allocator = step.owner.allocator;
|
|
|
|
// Get the cache directory path using the same logic as cache_config.zig
|
|
const cache_dir = getCacheDir(allocator) catch |err| {
|
|
std.debug.print("Warning: Could not determine cache directory: {s}\n", .{@errorName(err)});
|
|
return;
|
|
};
|
|
defer allocator.free(cache_dir);
|
|
|
|
// Check if cache directory exists before trying to delete
|
|
std.fs.cwd().access(cache_dir, .{}) catch {
|
|
// Cache doesn't exist, nothing to do
|
|
std.debug.print("Roc cache not found (nothing to clear)\n", .{});
|
|
return;
|
|
};
|
|
|
|
// Try to delete the cache directory
|
|
std.fs.cwd().deleteTree(cache_dir) catch |err| {
|
|
std.debug.print("Warning: Could not clear cache at {s}: {s}\n", .{ cache_dir, @errorName(err) });
|
|
return;
|
|
};
|
|
|
|
std.debug.print("Cleared roc cache at {s}\n", .{cache_dir});
|
|
}
|
|
|
|
/// Get the Roc cache directory path (matches cache_config.zig logic)
|
|
fn getCacheDir(allocator: std.mem.Allocator) ![]u8 {
|
|
const cache_dir_name = switch (builtin.os.tag) {
|
|
.windows => "Roc",
|
|
else => "roc",
|
|
};
|
|
|
|
// Respect XDG_CACHE_HOME if set
|
|
if (std.process.getEnvVarOwned(allocator, "XDG_CACHE_HOME")) |xdg_cache| {
|
|
defer allocator.free(xdg_cache);
|
|
return std.fs.path.join(allocator, &[_][]const u8{ xdg_cache, cache_dir_name });
|
|
} else |_| {
|
|
// Fall back to platform defaults
|
|
const home_env = switch (builtin.os.tag) {
|
|
.windows => "APPDATA",
|
|
else => "HOME",
|
|
};
|
|
|
|
const home_dir = std.process.getEnvVarOwned(allocator, home_env) catch {
|
|
return error.NoHomeDirectory;
|
|
};
|
|
defer allocator.free(home_dir);
|
|
|
|
return switch (builtin.os.tag) {
|
|
.linux => std.fs.path.join(allocator, &[_][]const u8{ home_dir, ".cache", cache_dir_name }),
|
|
.macos => std.fs.path.join(allocator, &[_][]const u8{ home_dir, "Library", "Caches", cache_dir_name }),
|
|
.windows => std.fs.path.join(allocator, &[_][]const u8{ home_dir, cache_dir_name }),
|
|
else => std.fs.path.join(allocator, &[_][]const u8{ home_dir, ".cache", cache_dir_name }),
|
|
};
|
|
}
|
|
}
|
|
};
|
|
|
|
const PrintBuildSuccessStep = struct {
|
|
step: Step,
|
|
|
|
fn create(b: *std.Build) *PrintBuildSuccessStep {
|
|
const self = b.allocator.create(PrintBuildSuccessStep) catch @panic("OOM");
|
|
self.* = .{
|
|
.step = Step.init(.{
|
|
.id = Step.Id.custom,
|
|
.name = "print-build-success",
|
|
.owner = b,
|
|
.makeFn = make,
|
|
}),
|
|
};
|
|
return self;
|
|
}
|
|
|
|
fn make(step: *Step, options: Step.MakeOptions) !void {
|
|
_ = step;
|
|
_ = options;
|
|
std.debug.print("Build succeeded!\n", .{});
|
|
}
|
|
};
|
|
|
|
/// Create a step that clears the Roc cache directory.
|
|
/// This is useful when rebuilding test platforms to ensure stale cached hosts aren't used.
|
|
fn createClearCacheStep(b: *std.Build) *Step {
|
|
const clear_cache = ClearRocCacheStep.create(b);
|
|
return &clear_cache.step;
|
|
}
|
|
|
|
fn setupTestPlatforms(
|
|
b: *std.Build,
|
|
target: ResolvedTarget,
|
|
optimize: OptimizeMode,
|
|
roc_modules: modules.RocModules,
|
|
test_platforms_step: *Step,
|
|
strip: bool,
|
|
omit_frame_pointer: ?bool,
|
|
) void {
|
|
// Clear the Roc cache when test platforms are rebuilt to ensure stale cached hosts aren't used
|
|
const clear_cache_step = createClearCacheStep(b);
|
|
const native_target_name = roc_target.RocTarget.fromStdTarget(target.result).toName();
|
|
|
|
// Build all test platforms for native target
|
|
for (all_test_platform_dirs) |platform_dir| {
|
|
const copy_step = buildAndCopyTestPlatformHostLib(
|
|
b,
|
|
platform_dir,
|
|
target,
|
|
native_target_name,
|
|
optimize,
|
|
roc_modules,
|
|
strip,
|
|
omit_frame_pointer,
|
|
);
|
|
clear_cache_step.dependOn(copy_step);
|
|
}
|
|
|
|
// Cross-compile for musl targets (glibc not needed for test-platforms step)
|
|
for (musl_cross_targets) |cross_target| {
|
|
const cross_resolved_target = b.resolveTargetQuery(cross_target.query);
|
|
|
|
for (all_test_platform_dirs) |platform_dir| {
|
|
const copy_step = buildAndCopyTestPlatformHostLib(
|
|
b,
|
|
platform_dir,
|
|
cross_resolved_target,
|
|
cross_target.name,
|
|
optimize,
|
|
roc_modules,
|
|
strip,
|
|
omit_frame_pointer,
|
|
);
|
|
clear_cache_step.dependOn(copy_step);
|
|
}
|
|
}
|
|
|
|
// Cross-compile for Windows targets
|
|
for (windows_cross_targets) |cross_target| {
|
|
const cross_resolved_target = b.resolveTargetQuery(cross_target.query);
|
|
|
|
for (all_test_platform_dirs) |platform_dir| {
|
|
const copy_step = buildAndCopyTestPlatformHostLib(
|
|
b,
|
|
platform_dir,
|
|
cross_resolved_target,
|
|
cross_target.name,
|
|
optimize,
|
|
roc_modules,
|
|
strip,
|
|
omit_frame_pointer,
|
|
);
|
|
clear_cache_step.dependOn(copy_step);
|
|
}
|
|
}
|
|
|
|
// Build the wasm test platform host for wasm32-freestanding
|
|
{
|
|
const wasm_target = b.resolveTargetQuery(.{ .cpu_arch = .wasm32, .os_tag = .freestanding, .abi = .none });
|
|
const copy_step = buildAndCopyTestPlatformHostLib(
|
|
b,
|
|
"wasm",
|
|
wasm_target,
|
|
"wasm32",
|
|
optimize,
|
|
roc_modules,
|
|
strip,
|
|
omit_frame_pointer,
|
|
);
|
|
clear_cache_step.dependOn(copy_step);
|
|
}
|
|
|
|
b.getInstallStep().dependOn(clear_cache_step);
|
|
test_platforms_step.dependOn(clear_cache_step);
|
|
}
|
|
|
|
pub fn build(b: *std.Build) void {
|
|
// build steps
|
|
const run_step = b.step("run", "Build and run the roc cli");
|
|
const roc_step = b.step("roc", "Build the roc compiler without running it");
|
|
const test_step = b.step("test", "Run all tests included in src/tests.zig");
|
|
const minici_step = b.step("minici", "Run a subset of CI build and test steps");
|
|
const checkfx_step = b.step("checkfx", "Check that every .roc file in test/fx has a corresponding test");
|
|
const fmt_step = b.step("fmt", "Format all zig code");
|
|
const check_fmt_step = b.step("check-fmt", "Check formatting of all zig code");
|
|
const snapshot_step = b.step("snapshot", "Run the snapshot tool to update snapshot files");
|
|
const playground_step = b.step("playground", "Build the WASM playground");
|
|
const playground_test_step = b.step("test-playground", "Build the integration test suite for the WASM playground");
|
|
const serialization_size_step = b.step("test-serialization-sizes", "Verify Serialized types have platform-independent sizes");
|
|
const wasm_static_lib_test_step = b.step("test-wasm-static-lib", "Test WASM static library builds with bytebox");
|
|
const test_cli_step = b.step("test-cli", "Test the roc CLI by running test programs");
|
|
const test_platforms_step = b.step("test-platforms", "Build test platform host libraries");
|
|
|
|
// general configuration
|
|
const target = blk: {
|
|
var default_target_query: std.Target.Query = .{
|
|
.abi = if (builtin.target.os.tag == .linux) .musl else null,
|
|
};
|
|
|
|
// Use baseline x86_64 CPU for Valgrind compatibility on CI (Valgrind 3.18.1 doesn't support AVX-512)
|
|
const is_ci = std.process.getEnvVarOwned(b.allocator, "CI") catch null;
|
|
if (is_ci != null and builtin.target.cpu.arch == .x86_64 and builtin.target.os.tag == .linux) {
|
|
default_target_query.cpu_model = .{ .explicit = &std.Target.x86.cpu.x86_64 };
|
|
}
|
|
|
|
break :blk b.standardTargetOptions(.{ .default_target = default_target_query });
|
|
};
|
|
const optimize = b.standardOptimizeOption(.{});
|
|
const strip_flag = b.option(bool, "strip", "Omit debug information");
|
|
const no_bin = b.option(bool, "no-bin", "Skip emitting binaries (important for fast incremental compilation)") orelse false;
|
|
const trace_eval = b.option(bool, "trace-eval", "Enable detailed evaluation tracing for debugging") orelse (optimize == .Debug);
|
|
const trace_refcount = b.option(bool, "trace-refcount", "Enable detailed refcount tracing for debugging memory issues") orelse false;
|
|
const trace_modules = b.option(bool, "trace-modules", "Enable module compilation and import resolution tracing") orelse false;
|
|
|
|
const parsed_args = parseBuildArgs(b);
|
|
const run_args = parsed_args.run_args;
|
|
const test_filters = parsed_args.test_filters;
|
|
|
|
// llvm configuration
|
|
const use_system_llvm = b.option(bool, "system-llvm", "Attempt to automatically detect and use system installed llvm") orelse false;
|
|
const enable_llvm = !use_system_llvm; // removed build flag `-Dllvm`, we include LLVM libraries by default now
|
|
const user_llvm_path = b.option([]const u8, "llvm-path", "Path to llvm. This path must contain the bin, lib, and include directory.");
|
|
// Since zig afl is broken currently, default to system afl.
|
|
const use_system_afl = b.option(bool, "system-afl", "Attempt to automatically detect and use system installed afl++") orelse true;
|
|
|
|
if (user_llvm_path) |path| {
|
|
// Even if the llvm backend is not enabled, still add the llvm path.
|
|
// AFL++ may use it for building fuzzing executables.
|
|
b.addSearchPrefix(b.pathJoin(&.{ path, "bin" }));
|
|
}
|
|
|
|
// tracy profiler configuration
|
|
const flag_enable_tracy = b.option([]const u8, "tracy", "Enable Tracy integration. Supply path to Tracy source");
|
|
const flag_tracy_callstack = b.option(bool, "tracy-callstack", "Include callstack information with Tracy data. Does nothing if -Dtracy is not provided") orelse false;
|
|
const flag_tracy_allocation = b.option(bool, "tracy-allocation", "Include allocation information with Tracy data. Does nothing if -Dtracy is not provided") orelse (flag_enable_tracy != null);
|
|
const flag_tracy_callstack_depth: u32 = b.option(u32, "tracy-callstack-depth", "Declare callstack depth for Tracy data. Does nothing if -Dtracy_callstack is not provided") orelse 10;
|
|
if (flag_tracy_callstack) {
|
|
std.log.warn("Tracy callstack is enable. This can significantly skew timings, but is important for understanding source location. Be cautious when generating timing and analyzing results.", .{});
|
|
}
|
|
|
|
// Create compile time build options
|
|
const build_options = b.addOptions();
|
|
build_options.addOption(bool, "enable_tracy", flag_enable_tracy != null);
|
|
build_options.addOption(bool, "trace_eval", trace_eval);
|
|
build_options.addOption(bool, "trace_refcount", trace_refcount);
|
|
build_options.addOption(bool, "trace_modules", trace_modules);
|
|
build_options.addOption([]const u8, "compiler_version", getCompilerVersion(b, optimize));
|
|
build_options.addOption(bool, "enable_tracy_callstack", flag_tracy_callstack);
|
|
build_options.addOption(bool, "enable_tracy_allocation", flag_tracy_allocation);
|
|
build_options.addOption(u32, "tracy_callstack_depth", flag_tracy_callstack_depth);
|
|
|
|
// Calculate effective strip value
|
|
// - If strip is explicitly set by user, use that (warn if tracy_callstack is also set)
|
|
// - Otherwise, default to stripping if not debug, unless tracy_callstack is enabled
|
|
const strip: bool = blk: {
|
|
if (strip_flag) |strip_bool| {
|
|
// User explicitly set strip
|
|
if (strip_bool and flag_tracy_callstack) {
|
|
std.log.warn("Both -Dstrip and -Dtracy-callstack are enabled. " ++
|
|
"Stripping will remove callstack information needed by Tracy.", .{});
|
|
}
|
|
break :blk strip_bool;
|
|
} else {
|
|
// User did not set strip - use defaults
|
|
if (flag_tracy_callstack) {
|
|
// Don't strip when tracy callstack is enabled (preserves debug info)
|
|
break :blk false;
|
|
} else {
|
|
// Default: strip in release modes
|
|
break :blk optimize != .Debug;
|
|
}
|
|
}
|
|
};
|
|
|
|
// Don't omit frame pointer when tracy callstack is enabled (needed for callstack capture)
|
|
const omit_frame_pointer: ?bool = if (flag_tracy_callstack) false else null;
|
|
|
|
const target_is_native =
|
|
// `query.isNative()` becomes false as soon as users override CPU features (e.g. -Dcpu=x86_64_v3),
|
|
// but we still want to treat those builds as native so macOS can link against real FSEvents.
|
|
target.result.os.tag == builtin.target.os.tag and
|
|
target.result.cpu.arch == builtin.target.cpu.arch and
|
|
target.result.abi == builtin.target.abi;
|
|
build_options.addOption(bool, "target_is_native", target_is_native);
|
|
|
|
// We use zstd for `roc bundle` and `roc unbundle` and downloading .tar.zst bundles.
|
|
const zstd = b.dependency("zstd", .{
|
|
.target = target,
|
|
.optimize = optimize,
|
|
});
|
|
|
|
const roc_modules = modules.RocModules.create(b, build_options, zstd);
|
|
|
|
// Build-time compiler for builtin .roc modules with caching
|
|
//
|
|
// Changes to .roc files in src/build/roc/ are automatically detected and trigger recompilation.
|
|
// However, if you modify the compiler itself (e.g., parse, can, check modules) and want those
|
|
// changes reflected in the builtin .bin files, you need to run: zig build rebuild-builtins
|
|
//
|
|
// We cache the builtin compiler executable to avoid ~doubling normal build times.
|
|
// CI always rebuilds from scratch, so it's not affected by this caching.
|
|
const builtin_roc_path = "src/build/roc/Builtin.roc";
|
|
|
|
// Check if we need to rebuild builtins by comparing .roc and .bin file timestamps
|
|
const should_rebuild_builtins = blk: {
|
|
const builtin_bin_path = "zig-out/builtins/Builtin.bin";
|
|
|
|
const roc_stat = std.fs.cwd().statFile(builtin_roc_path) catch break :blk true;
|
|
const bin_stat = std.fs.cwd().statFile(builtin_bin_path) catch break :blk true;
|
|
|
|
// If .roc file is newer than .bin file, rebuild
|
|
if (roc_stat.mtime > bin_stat.mtime) {
|
|
break :blk true;
|
|
}
|
|
|
|
// Check if builtin_indices.bin exists
|
|
_ = std.fs.cwd().statFile("zig-out/builtins/builtin_indices.bin") catch break :blk true;
|
|
|
|
// Builtin.bin exists and is up-to-date
|
|
break :blk false;
|
|
};
|
|
|
|
const write_compiled_builtins = b.addWriteFiles();
|
|
|
|
// Regenerate .bin files if necessary
|
|
if (should_rebuild_builtins) {
|
|
const run_builtin_compiler = createAndRunBuiltinCompiler(b, roc_modules, flag_enable_tracy, &.{builtin_roc_path});
|
|
write_compiled_builtins.step.dependOn(&run_builtin_compiler.step);
|
|
}
|
|
|
|
// Copy Builtin.bin from zig-out/builtins/
|
|
_ = write_compiled_builtins.addCopyFile(
|
|
.{ .cwd_relative = "zig-out/builtins/Builtin.bin" },
|
|
"Builtin.bin",
|
|
);
|
|
|
|
// Copy the source Builtin.roc file for embedding
|
|
_ = write_compiled_builtins.addCopyFile(
|
|
b.path(builtin_roc_path),
|
|
"Builtin.roc",
|
|
);
|
|
|
|
// Copy builtin_indices.bin
|
|
_ = write_compiled_builtins.addCopyFile(
|
|
.{ .cwd_relative = "zig-out/builtins/builtin_indices.bin" },
|
|
"builtin_indices.bin",
|
|
);
|
|
|
|
// Generate compiled_builtins.zig with hardcoded Builtin module
|
|
const builtins_source_str =
|
|
\\pub const builtin_bin = @embedFile("Builtin.bin");
|
|
\\pub const builtin_source = @embedFile("Builtin.roc");
|
|
\\pub const builtin_indices_bin = @embedFile("builtin_indices.bin");
|
|
\\
|
|
;
|
|
|
|
const compiled_builtins_source = write_compiled_builtins.add(
|
|
"compiled_builtins.zig",
|
|
builtins_source_str,
|
|
);
|
|
|
|
const compiled_builtins_module = b.createModule(.{
|
|
.root_source_file = compiled_builtins_source,
|
|
});
|
|
|
|
roc_modules.repl.addImport("compiled_builtins", compiled_builtins_module);
|
|
roc_modules.compile.addImport("compiled_builtins", compiled_builtins_module);
|
|
roc_modules.eval.addImport("compiled_builtins", compiled_builtins_module);
|
|
|
|
// Setup test platform host libraries
|
|
setupTestPlatforms(b, target, optimize, roc_modules, test_platforms_step, strip, omit_frame_pointer);
|
|
|
|
const roc_exe = addMainExe(b, roc_modules, target, optimize, strip, omit_frame_pointer, enable_llvm, use_system_llvm, user_llvm_path, flag_enable_tracy, zstd, compiled_builtins_module, write_compiled_builtins, flag_enable_tracy) orelse return;
|
|
roc_modules.addAll(roc_exe);
|
|
install_and_run(b, no_bin, roc_exe, roc_step, run_step, run_args);
|
|
|
|
// Clear the Roc cache when building the compiler to ensure stale cached artifacts aren't used
|
|
const clear_cache_step = createClearCacheStep(b);
|
|
roc_step.dependOn(clear_cache_step);
|
|
b.getInstallStep().dependOn(clear_cache_step);
|
|
|
|
// Unified test platform runner (replaces fx_cross_runner and int_cross_runner)
|
|
const test_runner_exe = b.addExecutable(.{
|
|
.name = "test_runner",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("src/cli/test/test_runner.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
}),
|
|
});
|
|
b.installArtifact(test_runner_exe);
|
|
|
|
// CLI integration tests - run actual roc programs like CI does
|
|
// These tests can run in parallel since each build uses content-hashed shim files
|
|
if (!no_bin) {
|
|
const install = b.addInstallArtifact(roc_exe, .{});
|
|
const install_runner = b.addInstallArtifact(test_runner_exe, .{});
|
|
|
|
// Test int platform (native mode only for now)
|
|
const run_int_tests = b.addRunArtifact(test_runner_exe);
|
|
run_int_tests.addArg("zig-out/bin/roc");
|
|
run_int_tests.addArg("int");
|
|
run_int_tests.addArg("--mode=native");
|
|
run_int_tests.step.dependOn(&install.step);
|
|
run_int_tests.step.dependOn(&install_runner.step);
|
|
run_int_tests.step.dependOn(test_platforms_step);
|
|
test_cli_step.dependOn(&run_int_tests.step);
|
|
|
|
// Test str platform (native mode only for now)
|
|
const run_str_tests = b.addRunArtifact(test_runner_exe);
|
|
run_str_tests.addArg("zig-out/bin/roc");
|
|
run_str_tests.addArg("str");
|
|
run_str_tests.addArg("--mode=native");
|
|
run_str_tests.step.dependOn(&install.step);
|
|
run_str_tests.step.dependOn(&install_runner.step);
|
|
run_str_tests.step.dependOn(test_platforms_step);
|
|
test_cli_step.dependOn(&run_str_tests.step);
|
|
|
|
// Roc subcommands integration test
|
|
const roc_subcommands_test = b.addTest(.{
|
|
.name = "roc_subcommands_test",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("src/cli/test/roc_subcommands.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
}),
|
|
.filters = test_filters,
|
|
});
|
|
|
|
const run_roc_subcommands_test = b.addRunArtifact(roc_subcommands_test);
|
|
if (run_args.len != 0) {
|
|
run_roc_subcommands_test.addArgs(run_args);
|
|
}
|
|
run_roc_subcommands_test.step.dependOn(&install.step);
|
|
run_roc_subcommands_test.step.dependOn(test_platforms_step);
|
|
test_cli_step.dependOn(&run_roc_subcommands_test.step);
|
|
}
|
|
|
|
// Manual rebuild command: zig build rebuild-builtins
|
|
// Use this after making compiler changes to ensure those changes are reflected in builtins
|
|
const rebuild_builtins_step = b.step(
|
|
"rebuild-builtins",
|
|
"Force rebuild of all builtin modules (*.roc -> *.bin)",
|
|
);
|
|
|
|
// Clean zig-out/ to ensure a fresh rebuild of builtins
|
|
// Note: We don't delete .zig-cache because it contains build options needed during compilation.
|
|
const clean_out_step = b.addRemoveDirTree(b.path("zig-out"));
|
|
|
|
// Also clear the roc cache to avoid stale cached modules with old struct layouts
|
|
const clear_roc_cache_step = createClearCacheStep(b);
|
|
|
|
// Discover .roc files again for the rebuild command
|
|
const roc_files_force = discoverBuiltinRocFiles(b) catch |err| {
|
|
std.debug.print("Failed to discover .roc files for rebuild: {}\n", .{err});
|
|
return;
|
|
};
|
|
|
|
const run_builtin_compiler_force = createAndRunBuiltinCompiler(b, roc_modules, flag_enable_tracy, roc_files_force);
|
|
run_builtin_compiler_force.step.dependOn(&clean_out_step.step);
|
|
run_builtin_compiler_force.step.dependOn(clear_roc_cache_step);
|
|
rebuild_builtins_step.dependOn(&run_builtin_compiler_force.step);
|
|
|
|
// Add the compiled builtins module to roc exe and make it depend on the builtins being ready
|
|
roc_exe.root_module.addImport("compiled_builtins", compiled_builtins_module);
|
|
roc_exe.step.dependOn(&write_compiled_builtins.step);
|
|
|
|
// Add snapshot tool
|
|
const snapshot_exe = b.addExecutable(.{
|
|
.name = "snapshot",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("src/snapshot_tool/main.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
.link_libc = true,
|
|
}),
|
|
});
|
|
configureBackend(snapshot_exe, target);
|
|
roc_modules.addAll(snapshot_exe);
|
|
snapshot_exe.root_module.addImport("compiled_builtins", compiled_builtins_module);
|
|
snapshot_exe.step.dependOn(&write_compiled_builtins.step);
|
|
add_tracy(b, roc_modules.build_options, snapshot_exe, target, false, flag_enable_tracy);
|
|
install_and_run(b, no_bin, snapshot_exe, snapshot_step, snapshot_step, run_args);
|
|
|
|
const playground_exe = b.addExecutable(.{
|
|
.name = "playground",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("src/playground_wasm/main.zig"),
|
|
.target = b.resolveTargetQuery(.{
|
|
.cpu_arch = .wasm32,
|
|
.os_tag = .freestanding,
|
|
}),
|
|
.optimize = optimize,
|
|
}),
|
|
});
|
|
configureBackend(playground_exe, b.resolveTargetQuery(.{
|
|
.cpu_arch = .wasm32,
|
|
.os_tag = .freestanding,
|
|
}));
|
|
playground_exe.entry = .disabled;
|
|
playground_exe.rdynamic = true;
|
|
playground_exe.link_function_sections = true;
|
|
playground_exe.import_memory = false;
|
|
roc_modules.addAll(playground_exe);
|
|
playground_exe.root_module.addImport("compiled_builtins", compiled_builtins_module);
|
|
playground_exe.step.dependOn(&write_compiled_builtins.step);
|
|
|
|
add_tracy(b, roc_modules.build_options, playground_exe, b.resolveTargetQuery(.{
|
|
.cpu_arch = .wasm32,
|
|
.os_tag = .freestanding,
|
|
}), false, null);
|
|
|
|
const playground_install = b.addInstallArtifact(playground_exe, .{});
|
|
playground_step.dependOn(&playground_install.step);
|
|
|
|
const bytebox = b.dependency("bytebox", .{
|
|
.target = target,
|
|
.optimize = optimize,
|
|
});
|
|
|
|
// Build playground integration tests - now enabled for all optimization modes
|
|
const playground_test_install = blk: {
|
|
const playground_integration_test_exe = b.addExecutable(.{
|
|
.name = "playground_integration_test",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("test/playground-integration/main.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
}),
|
|
});
|
|
configureBackend(playground_integration_test_exe, target);
|
|
playground_integration_test_exe.root_module.addImport("bytebox", bytebox.module("bytebox"));
|
|
playground_integration_test_exe.root_module.addImport("build_options", build_options.createModule());
|
|
roc_modules.addAll(playground_integration_test_exe);
|
|
|
|
const install = b.addInstallArtifact(playground_integration_test_exe, .{});
|
|
// Ensure playground WASM is built before running the integration test
|
|
install.step.dependOn(&playground_install.step);
|
|
playground_test_step.dependOn(&install.step);
|
|
|
|
const run_playground_test = b.addRunArtifact(playground_integration_test_exe);
|
|
if (run_args.len != 0) {
|
|
run_playground_test.addArgs(run_args);
|
|
}
|
|
run_playground_test.step.dependOn(&install.step);
|
|
playground_test_step.dependOn(&run_playground_test.step);
|
|
|
|
break :blk install;
|
|
};
|
|
|
|
// Add serialization size check
|
|
// This verifies that Serialized types have the same size on 32-bit and 64-bit platforms
|
|
// using compile-time assertions
|
|
{
|
|
// Build for native - will fail at compile time if sizes don't match expected
|
|
const size_check_native = b.addExecutable(.{
|
|
.name = "serialization_size_check_native",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("test/serialization_size_check.zig"),
|
|
.target = target,
|
|
.optimize = .Debug,
|
|
}),
|
|
});
|
|
configureBackend(size_check_native, target);
|
|
roc_modules.addAll(size_check_native);
|
|
|
|
// Build for wasm32 (32-bit) - will fail at compile time if sizes don't match expected
|
|
const size_check_wasm32 = b.addExecutable(.{
|
|
.name = "serialization_size_check_wasm32",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("test/serialization_size_check.zig"),
|
|
.target = b.resolveTargetQuery(.{
|
|
.cpu_arch = .wasm32,
|
|
.os_tag = .freestanding,
|
|
}),
|
|
.optimize = .Debug,
|
|
}),
|
|
});
|
|
configureBackend(size_check_wasm32, b.resolveTargetQuery(.{
|
|
.cpu_arch = .wasm32,
|
|
.os_tag = .freestanding,
|
|
}));
|
|
size_check_wasm32.entry = .disabled;
|
|
size_check_wasm32.rdynamic = true;
|
|
roc_modules.addAll(size_check_wasm32);
|
|
|
|
// Run the native version to confirm (wasm32 build is enough to verify 32-bit)
|
|
const run_native = b.addRunArtifact(size_check_native);
|
|
|
|
// The test passes if both executables build successfully (compile-time checks pass)
|
|
// and the native one runs without error
|
|
serialization_size_step.dependOn(&size_check_native.step);
|
|
serialization_size_step.dependOn(&size_check_wasm32.step);
|
|
serialization_size_step.dependOn(&run_native.step);
|
|
}
|
|
|
|
// Build WASM static library test runner with bytebox
|
|
// This test requires the WASM file to be built separately via `roc build test/wasm/app.roc --target=wasm32`
|
|
{
|
|
const wasm_test_exe = b.addExecutable(.{
|
|
.name = "wasm_static_lib_test",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("test/wasm/main.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
}),
|
|
});
|
|
configureBackend(wasm_test_exe, target);
|
|
wasm_test_exe.root_module.addImport("bytebox", bytebox.module("bytebox"));
|
|
|
|
const install = b.addInstallArtifact(wasm_test_exe, .{});
|
|
wasm_static_lib_test_step.dependOn(&install.step);
|
|
|
|
const run_wasm_test = b.addRunArtifact(wasm_test_exe);
|
|
if (run_args.len != 0) {
|
|
run_wasm_test.addArgs(run_args);
|
|
}
|
|
run_wasm_test.step.dependOn(&install.step);
|
|
wasm_static_lib_test_step.dependOn(&run_wasm_test.step);
|
|
}
|
|
|
|
// Check fx platform test coverage convenience step
|
|
const checkfx_inner = CheckFxStep.create(b);
|
|
checkfx_step.dependOn(&checkfx_inner.step);
|
|
|
|
// Mini CI convenience step: runs a sequence of common build and test commands in order.
|
|
const minici_inner = MiniCiStep.create(b);
|
|
minici_step.dependOn(&minici_inner.step);
|
|
|
|
// Create and add module tests
|
|
const module_tests_result = roc_modules.createModuleTests(b, target, optimize, zstd, test_filters);
|
|
const tests_summary = TestsSummaryStep.create(b, test_filters, module_tests_result.forced_passes);
|
|
for (module_tests_result.tests) |module_test| {
|
|
// Add compiled builtins to check, repl, and eval module tests
|
|
if (std.mem.eql(u8, module_test.test_step.name, "check") or std.mem.eql(u8, module_test.test_step.name, "repl") or std.mem.eql(u8, module_test.test_step.name, "eval")) {
|
|
module_test.test_step.root_module.addImport("compiled_builtins", compiled_builtins_module);
|
|
module_test.test_step.step.dependOn(&write_compiled_builtins.step);
|
|
}
|
|
|
|
if (run_args.len != 0) {
|
|
module_test.run_step.addArgs(run_args);
|
|
}
|
|
|
|
// Create individual test step for this module
|
|
const test_exe_name = module_test.test_step.name;
|
|
const step_name = b.fmt("test-{s}", .{test_exe_name});
|
|
const individual_test_step = b.step(step_name, b.fmt("Run {s} tests only", .{test_exe_name}));
|
|
|
|
// Create run step that accepts command line args (including --test-filter)
|
|
const individual_run = b.addRunArtifact(module_test.test_step);
|
|
if (run_args.len != 0) {
|
|
individual_run.addArgs(run_args);
|
|
}
|
|
individual_test_step.dependOn(&individual_run.step);
|
|
|
|
b.default_step.dependOn(&module_test.test_step.step);
|
|
tests_summary.addRun(&module_test.run_step.step);
|
|
}
|
|
|
|
// Add snapshot tool test
|
|
const enable_snapshot_tests = b.option(bool, "snapshot-tests", "Enable snapshot tests") orelse true;
|
|
if (enable_snapshot_tests) {
|
|
const snapshot_test = b.addTest(.{
|
|
.name = "snapshot_tool_test",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("src/snapshot_tool/main.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
.link_libc = true,
|
|
}),
|
|
.filters = test_filters,
|
|
});
|
|
roc_modules.addAll(snapshot_test);
|
|
snapshot_test.root_module.addImport("compiled_builtins", compiled_builtins_module);
|
|
snapshot_test.step.dependOn(&write_compiled_builtins.step);
|
|
add_tracy(b, roc_modules.build_options, snapshot_test, target, false, flag_enable_tracy);
|
|
|
|
const run_snapshot_test = b.addRunArtifact(snapshot_test);
|
|
if (run_args.len != 0) {
|
|
run_snapshot_test.addArgs(run_args);
|
|
}
|
|
tests_summary.addRun(&run_snapshot_test.step);
|
|
}
|
|
|
|
// Add CLI test
|
|
const enable_cli_tests = b.option(bool, "cli-tests", "Enable cli tests") orelse true;
|
|
if (enable_cli_tests) {
|
|
const cli_test = b.addTest(.{
|
|
.name = "cli_test",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("src/cli/main.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
.link_libc = true,
|
|
}),
|
|
.filters = test_filters,
|
|
});
|
|
roc_modules.addAll(cli_test);
|
|
cli_test.linkLibrary(zstd.artifact("zstd"));
|
|
add_tracy(b, roc_modules.build_options, cli_test, target, false, flag_enable_tracy);
|
|
cli_test.root_module.addImport("compiled_builtins", compiled_builtins_module);
|
|
cli_test.step.dependOn(&write_compiled_builtins.step);
|
|
|
|
const run_cli_test = b.addRunArtifact(cli_test);
|
|
if (run_args.len != 0) {
|
|
run_cli_test.addArgs(run_args);
|
|
}
|
|
tests_summary.addRun(&run_cli_test.step);
|
|
}
|
|
|
|
// Add watch tests
|
|
const enable_watch_tests = b.option(bool, "watch-tests", "Enable watch tests") orelse true;
|
|
if (enable_watch_tests) {
|
|
const watch_test = b.addTest(.{
|
|
.name = "watch_test",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("src/watch/watch.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
.link_libc = true,
|
|
}),
|
|
.filters = test_filters,
|
|
});
|
|
roc_modules.addAll(watch_test);
|
|
add_tracy(b, roc_modules.build_options, watch_test, target, false, flag_enable_tracy);
|
|
|
|
// Link platform-specific libraries for file watching
|
|
if (target.result.os.tag == .macos and target_is_native) {
|
|
watch_test.linkFramework("CoreFoundation");
|
|
watch_test.linkFramework("CoreServices");
|
|
} else if (target.result.os.tag == .windows) {
|
|
watch_test.linkSystemLibrary("kernel32");
|
|
}
|
|
|
|
const run_watch_test = b.addRunArtifact(watch_test);
|
|
if (run_args.len != 0) {
|
|
run_watch_test.addArgs(run_args);
|
|
}
|
|
tests_summary.addRun(&run_watch_test.step);
|
|
}
|
|
|
|
// Add check for forbidden patterns in type checker code
|
|
const check_patterns = CheckTypeCheckerPatternsStep.create(b);
|
|
test_step.dependOn(&check_patterns.step);
|
|
|
|
// Add check for @enumFromInt(0) usage
|
|
const check_enum_from_int = CheckEnumFromIntZeroStep.create(b);
|
|
test_step.dependOn(&check_enum_from_int.step);
|
|
|
|
// Add check for unused variable suppression patterns
|
|
const check_unused = CheckUnusedSuppressionStep.create(b);
|
|
test_step.dependOn(&check_unused.step);
|
|
|
|
// Check for @panic and std.debug.panic in interpreter and builtins
|
|
const check_panic = CheckPanicStep.create(b);
|
|
test_step.dependOn(&check_panic.step);
|
|
|
|
// Add check for global stdio usage in CLI code
|
|
const check_cli_stdio = CheckCliGlobalStdioStep.create(b);
|
|
test_step.dependOn(&check_cli_stdio.step);
|
|
|
|
test_step.dependOn(&tests_summary.step);
|
|
|
|
b.default_step.dependOn(playground_step);
|
|
{
|
|
const install = playground_test_install;
|
|
b.default_step.dependOn(&install.step);
|
|
}
|
|
|
|
// Fmt zig code.
|
|
const fmt_paths = .{ "src", "build.zig" };
|
|
const fmt = b.addFmt(.{ .paths = &fmt_paths });
|
|
fmt_step.dependOn(&fmt.step);
|
|
|
|
const check_fmt = b.addFmt(.{ .paths = &fmt_paths, .check = true });
|
|
check_fmt_step.dependOn(&check_fmt.step);
|
|
|
|
const fuzz = b.option(bool, "fuzz", "Build fuzz targets including AFL++ and tooling") orelse false;
|
|
const is_windows = target.result.os.tag == .windows;
|
|
|
|
// fx platform effectful functions test - only run when not cross-compiling
|
|
if (isNativeishOrMusl(target)) {
|
|
// Determine the appropriate target for the fx platform host library.
|
|
// On Linux, we need to use musl explicitly because the CLI's findHostLibrary
|
|
// looks for targets/x64musl/libhost.a first, and musl produces proper static binaries.
|
|
const native_fx_target_dir = roc_target.RocTarget.fromStdTarget(target.result).toName();
|
|
const fx_host_target, const fx_host_target_dir: ?[]const u8 = switch (target.result.os.tag) {
|
|
.linux => switch (target.result.cpu.arch) {
|
|
.x86_64 => .{ b.resolveTargetQuery(.{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .musl }), "x64musl" },
|
|
.aarch64 => .{ b.resolveTargetQuery(.{ .cpu_arch = .aarch64, .os_tag = .linux, .abi = .musl }), "arm64musl" },
|
|
else => .{ target, native_fx_target_dir },
|
|
},
|
|
.windows => switch (target.result.cpu.arch) {
|
|
.x86_64 => .{ target, "x64win" },
|
|
.aarch64 => .{ target, "arm64win" },
|
|
else => .{ target, native_fx_target_dir },
|
|
},
|
|
else => .{ target, native_fx_target_dir },
|
|
};
|
|
|
|
// Create fx test platform host static library
|
|
const test_platform_fx_host_lib = createTestPlatformHostLib(
|
|
b,
|
|
"test_platform_fx_host",
|
|
"test/fx/platform/host.zig",
|
|
fx_host_target,
|
|
optimize,
|
|
roc_modules,
|
|
strip,
|
|
omit_frame_pointer,
|
|
);
|
|
|
|
// Copy the fx test platform host library to the source directory
|
|
const copy_test_fx_host = b.addUpdateSourceFiles();
|
|
const test_fx_host_filename = if (target.result.os.tag == .windows) "host.lib" else "libhost.a";
|
|
copy_test_fx_host.addCopyFileToSource(test_platform_fx_host_lib.getEmittedBin(), b.pathJoin(&.{ "test/fx/platform", test_fx_host_filename }));
|
|
b.getInstallStep().dependOn(©_test_fx_host.step);
|
|
|
|
// Also copy to the target-specific directory so findHostLibrary finds it
|
|
if (fx_host_target_dir) |target_dir| {
|
|
copy_test_fx_host.addCopyFileToSource(
|
|
test_platform_fx_host_lib.getEmittedBin(),
|
|
b.pathJoin(&.{ "test/fx/platform/targets", target_dir, test_fx_host_filename }),
|
|
);
|
|
}
|
|
|
|
const fx_platform_test = b.addTest(.{
|
|
.name = "fx_platform_test",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("src/cli/test/fx_platform_test.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
}),
|
|
.filters = test_filters,
|
|
});
|
|
|
|
const run_fx_platform_test = b.addRunArtifact(fx_platform_test);
|
|
if (run_args.len != 0) {
|
|
run_fx_platform_test.addArgs(run_args);
|
|
}
|
|
// Ensure host library is copied before running the test
|
|
run_fx_platform_test.step.dependOn(©_test_fx_host.step);
|
|
// Ensure roc binary is built before running the test (tests invoke roc CLI)
|
|
run_fx_platform_test.step.dependOn(roc_step);
|
|
tests_summary.addRun(&run_fx_platform_test.step);
|
|
}
|
|
|
|
var build_afl = false;
|
|
if (!isNativeishOrMusl(target)) {
|
|
std.log.warn("Cross compilation does not support fuzzing (Only building repro executables)", .{});
|
|
} else if (is_windows) {
|
|
// Windows does not support fuzzing - only build repro executables
|
|
} else if (use_system_afl) {
|
|
// If we have system afl, no need for llvm-config.
|
|
build_afl = true;
|
|
} else {
|
|
// AFL++ does not work with our prebuilt static llvm.
|
|
// Check for llvm-config program in user_llvm_path or on the system.
|
|
// If found, let AFL++ use that.
|
|
if (b.findProgram(&.{"llvm-config"}, &.{})) |_| {
|
|
build_afl = true;
|
|
} else |_| {
|
|
std.log.warn("AFL++ requires a full version of llvm from the system or passed in via -Dllvm-path, but `llvm-config` was not found (Only building repro executables)", .{});
|
|
}
|
|
}
|
|
|
|
const names: []const []const u8 = &.{
|
|
"tokenize",
|
|
"parse",
|
|
"canonicalize",
|
|
};
|
|
for (names) |name| {
|
|
add_fuzz_target(
|
|
b,
|
|
fuzz,
|
|
build_afl,
|
|
use_system_afl,
|
|
no_bin,
|
|
run_args,
|
|
target,
|
|
optimize,
|
|
roc_modules,
|
|
flag_enable_tracy,
|
|
name,
|
|
);
|
|
}
|
|
}
|
|
|
|
const ModuleTest = modules.ModuleTest;
|
|
|
|
fn discoverBuiltinRocFiles(b: *std.Build) ![]const []const u8 {
|
|
const builtin_roc_path = try b.build_root.join(b.allocator, &.{ "src", "build", "roc" });
|
|
var builtin_roc_dir = try std.fs.openDirAbsolute(builtin_roc_path, .{ .iterate = true });
|
|
defer builtin_roc_dir.close();
|
|
|
|
var roc_files = std.ArrayList([]const u8).empty;
|
|
errdefer roc_files.deinit(b.allocator);
|
|
|
|
var iter = builtin_roc_dir.iterate();
|
|
while (try iter.next()) |entry| {
|
|
if (entry.kind == .file and std.mem.endsWith(u8, entry.name, ".roc")) {
|
|
const full_path = b.fmt("src/build/roc/{s}", .{entry.name});
|
|
try roc_files.append(b.allocator, full_path);
|
|
}
|
|
}
|
|
|
|
return roc_files.toOwnedSlice(b.allocator);
|
|
}
|
|
|
|
fn generateCompiledBuiltinsSource(b: *std.Build, roc_files: []const []const u8) ![]const u8 {
|
|
var builtins_source = std.ArrayList(u8).empty;
|
|
errdefer builtins_source.deinit(b.allocator);
|
|
const writer = builtins_source.writer(b.allocator);
|
|
|
|
for (roc_files) |roc_path| {
|
|
const roc_basename = std.fs.path.basename(roc_path);
|
|
const name_without_ext = roc_basename[0 .. roc_basename.len - 4];
|
|
// Use lowercase with underscore for the identifier
|
|
const lower_name = try std.ascii.allocLowerString(b.allocator, name_without_ext);
|
|
|
|
try writer.print("pub const {s}_bin = @embedFile(\"{s}.bin\");\n", .{
|
|
lower_name,
|
|
name_without_ext,
|
|
});
|
|
|
|
// Also embed the source .roc file
|
|
try writer.print("pub const {s}_source = @embedFile(\"{s}\");\n", .{
|
|
lower_name,
|
|
roc_basename,
|
|
});
|
|
}
|
|
|
|
// Also embed builtin_indices.bin
|
|
try writer.writeAll("pub const builtin_indices_bin = @embedFile(\"builtin_indices.bin\");\n");
|
|
|
|
return builtins_source.toOwnedSlice(b.allocator);
|
|
}
|
|
|
|
fn add_fuzz_target(
|
|
b: *std.Build,
|
|
fuzz: bool,
|
|
build_afl: bool,
|
|
use_system_afl: bool,
|
|
no_bin: bool,
|
|
run_args: []const []const u8,
|
|
target: ResolvedTarget,
|
|
optimize: OptimizeMode,
|
|
roc_modules: modules.RocModules,
|
|
tracy: ?[]const u8,
|
|
name: []const u8,
|
|
) void {
|
|
// We always include the repro scripts (no dependencies).
|
|
// We only include the fuzzing scripts if `-Dfuzz` is set.
|
|
const root_source_file = b.path(b.fmt("test/fuzzing/fuzz-{s}.zig", .{name}));
|
|
const fuzz_obj = b.addObject(.{
|
|
.name = b.fmt("{s}_obj", .{name}),
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = root_source_file,
|
|
.target = target,
|
|
// Work around instrumentation bugs on mac without giving up perf on linux.
|
|
.optimize = if (target.result.os.tag == .macos) .Debug else .ReleaseSafe,
|
|
}),
|
|
});
|
|
configureBackend(fuzz_obj, target);
|
|
// Required for fuzzing.
|
|
fuzz_obj.root_module.link_libc = true;
|
|
fuzz_obj.root_module.stack_check = false;
|
|
|
|
roc_modules.addAll(fuzz_obj);
|
|
add_tracy(b, roc_modules.build_options, fuzz_obj, target, false, tracy);
|
|
|
|
const name_exe = b.fmt("fuzz-{s}", .{name});
|
|
const name_repro = b.fmt("repro-{s}", .{name});
|
|
const repro_step = b.step(name_repro, b.fmt("run fuzz reproduction for {s}", .{name}));
|
|
const repro_exe = b.addExecutable(.{
|
|
.name = name_repro,
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("test/fuzzing/fuzz-repro.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
.link_libc = true,
|
|
}),
|
|
});
|
|
configureBackend(repro_exe, target);
|
|
repro_exe.root_module.addImport("fuzz_test", fuzz_obj.root_module);
|
|
|
|
install_and_run(b, no_bin, repro_exe, repro_step, repro_step, run_args);
|
|
|
|
if (fuzz and build_afl and !no_bin) {
|
|
const fuzz_step = b.step(name_exe, b.fmt("Generate fuzz executable for {s}", .{name}));
|
|
b.default_step.dependOn(fuzz_step);
|
|
|
|
const afl = b.lazyImport(@This(), "afl_kit") orelse return;
|
|
const fuzz_exe = afl.addInstrumentedExe(b, target, .ReleaseSafe, &.{}, use_system_afl, fuzz_obj, &.{"-lm"}) orelse return;
|
|
const install_fuzz = b.addInstallBinFile(fuzz_exe, name_exe);
|
|
fuzz_step.dependOn(&install_fuzz.step);
|
|
b.getInstallStep().dependOn(&install_fuzz.step);
|
|
}
|
|
}
|
|
|
|
fn addMainExe(
|
|
b: *std.Build,
|
|
roc_modules: modules.RocModules,
|
|
target: ResolvedTarget,
|
|
optimize: OptimizeMode,
|
|
strip: bool,
|
|
omit_frame_pointer: ?bool,
|
|
enable_llvm: bool,
|
|
use_system_llvm: bool,
|
|
user_llvm_path: ?[]const u8,
|
|
tracy: ?[]const u8,
|
|
zstd: *Dependency,
|
|
compiled_builtins_module: *std.Build.Module,
|
|
write_compiled_builtins: *Step.WriteFile,
|
|
flag_enable_tracy: ?[]const u8,
|
|
) ?*Step.Compile {
|
|
const exe = b.addExecutable(.{
|
|
.name = "roc",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("src/cli/main.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
.strip = strip,
|
|
.omit_frame_pointer = omit_frame_pointer,
|
|
.link_libc = true,
|
|
}),
|
|
});
|
|
configureBackend(exe, target);
|
|
|
|
// Build str and int test platform host libraries for native target
|
|
// (fx and fx-open are only built via test-platforms step)
|
|
const main_build_platforms = [_][]const u8{ "str", "int" };
|
|
const native_target_name = roc_target.RocTarget.fromStdTarget(target.result).toName();
|
|
|
|
for (main_build_platforms) |platform_dir| {
|
|
const copy_step = buildAndCopyTestPlatformHostLib(
|
|
b,
|
|
platform_dir,
|
|
target,
|
|
native_target_name,
|
|
optimize,
|
|
roc_modules,
|
|
strip,
|
|
omit_frame_pointer,
|
|
);
|
|
b.getInstallStep().dependOn(copy_step);
|
|
}
|
|
|
|
// Cross-compile for all Linux targets (musl + glibc)
|
|
for (linux_cross_targets) |cross_target| {
|
|
const cross_resolved_target = b.resolveTargetQuery(cross_target.query);
|
|
|
|
for (main_build_platforms) |platform_dir| {
|
|
const copy_step = buildAndCopyTestPlatformHostLib(
|
|
b,
|
|
platform_dir,
|
|
cross_resolved_target,
|
|
cross_target.name,
|
|
optimize,
|
|
roc_modules,
|
|
strip,
|
|
omit_frame_pointer,
|
|
);
|
|
b.getInstallStep().dependOn(copy_step);
|
|
}
|
|
|
|
// Generate glibc stubs for gnu targets
|
|
if (cross_target.query.abi == .gnu) {
|
|
const glibc_stub = generateGlibcStub(b, cross_resolved_target, cross_target.name);
|
|
if (glibc_stub) |stub| {
|
|
b.getInstallStep().dependOn(&stub.step);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create builtins static library at build time with minimal dependencies
|
|
const builtins_obj = b.addObject(.{
|
|
.name = "roc_builtins",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("src/builtins/static_lib.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
.strip = strip,
|
|
.omit_frame_pointer = omit_frame_pointer,
|
|
.pic = true, // Enable Position Independent Code for PIE compatibility
|
|
}),
|
|
});
|
|
configureBackend(builtins_obj, target);
|
|
|
|
// Create shim static library at build time - fully static without libc
|
|
//
|
|
// NOTE we do NOT link libC here to avoid dynamic dependency on libC
|
|
const shim_lib = b.addLibrary(.{
|
|
.name = "roc_interpreter_shim",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("src/interpreter_shim/main.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
.strip = strip,
|
|
.omit_frame_pointer = omit_frame_pointer,
|
|
.pic = true, // Enable Position Independent Code for PIE compatibility
|
|
}),
|
|
.linkage = .static,
|
|
});
|
|
configureBackend(shim_lib, target);
|
|
// Add all modules from roc_modules that the shim needs
|
|
roc_modules.addAll(shim_lib);
|
|
// Add compiled builtins module for loading builtin types
|
|
shim_lib.root_module.addImport("compiled_builtins", compiled_builtins_module);
|
|
shim_lib.step.dependOn(&write_compiled_builtins.step);
|
|
// Link against the pre-built builtins library
|
|
shim_lib.addObject(builtins_obj);
|
|
// Bundle compiler-rt for our math symbols
|
|
shim_lib.bundle_compiler_rt = true;
|
|
// Install shim library to the output directory
|
|
const install_shim = b.addInstallArtifact(shim_lib, .{});
|
|
b.getInstallStep().dependOn(&install_shim.step);
|
|
// Copy the shim library to the src/ directory for embedding as binary data
|
|
// This is because @embedFile happens at compile time and needs the file to exist already
|
|
// and zig doesn't permit embedding files from directories outside the source tree.
|
|
const copy_shim = b.addUpdateSourceFiles();
|
|
const interpreter_shim_filename = if (target.result.os.tag == .windows) "roc_interpreter_shim.lib" else "libroc_interpreter_shim.a";
|
|
copy_shim.addCopyFileToSource(shim_lib.getEmittedBin(), b.pathJoin(&.{ "src/cli", interpreter_shim_filename }));
|
|
exe.step.dependOn(©_shim.step);
|
|
|
|
// Add tracy support (required by parse/can/check modules)
|
|
add_tracy(b, roc_modules.build_options, shim_lib, b.graph.host, false, flag_enable_tracy);
|
|
|
|
// Cross-compile interpreter shim for all supported targets
|
|
// This allows `roc build --target=X` to work for cross-compilation
|
|
const cross_compile_shim_targets = [_]struct { name: []const u8, query: std.Target.Query }{
|
|
.{ .name = "x64musl", .query = .{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .musl } },
|
|
.{ .name = "arm64musl", .query = .{ .cpu_arch = .aarch64, .os_tag = .linux, .abi = .musl } },
|
|
.{ .name = "x64glibc", .query = .{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .gnu } },
|
|
.{ .name = "arm64glibc", .query = .{ .cpu_arch = .aarch64, .os_tag = .linux, .abi = .gnu } },
|
|
.{ .name = "wasm32", .query = .{ .cpu_arch = .wasm32, .os_tag = .freestanding, .abi = .none } },
|
|
};
|
|
|
|
for (cross_compile_shim_targets) |cross_target| {
|
|
const cross_resolved_target = b.resolveTargetQuery(cross_target.query);
|
|
|
|
// Build builtins object for this target
|
|
const cross_builtins_obj = b.addObject(.{
|
|
.name = b.fmt("roc_builtins_{s}", .{cross_target.name}),
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("src/builtins/static_lib.zig"),
|
|
.target = cross_resolved_target,
|
|
.optimize = optimize,
|
|
.strip = strip,
|
|
.omit_frame_pointer = omit_frame_pointer,
|
|
.pic = true,
|
|
}),
|
|
});
|
|
configureBackend(cross_builtins_obj, cross_resolved_target);
|
|
|
|
// Build interpreter shim library for this target
|
|
const cross_shim_lib = b.addLibrary(.{
|
|
.name = b.fmt("roc_interpreter_shim_{s}", .{cross_target.name}),
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("src/interpreter_shim/main.zig"),
|
|
.target = cross_resolved_target,
|
|
.optimize = optimize,
|
|
.strip = strip,
|
|
.omit_frame_pointer = omit_frame_pointer,
|
|
.pic = true,
|
|
}),
|
|
.linkage = .static,
|
|
});
|
|
configureBackend(cross_shim_lib, cross_resolved_target);
|
|
|
|
// For wasm32, only add the modules needed by the interpreter shim
|
|
// (compile, watch, lsp, repl, ipc use threading/file I/O not available on freestanding)
|
|
if (cross_target.query.cpu_arch == .wasm32 and cross_target.query.os_tag == .freestanding) {
|
|
cross_shim_lib.root_module.addImport("base", roc_modules.base);
|
|
cross_shim_lib.root_module.addImport("collections", roc_modules.collections);
|
|
cross_shim_lib.root_module.addImport("types", roc_modules.types);
|
|
cross_shim_lib.root_module.addImport("builtins", roc_modules.builtins);
|
|
cross_shim_lib.root_module.addImport("can", roc_modules.can);
|
|
cross_shim_lib.root_module.addImport("check", roc_modules.check);
|
|
cross_shim_lib.root_module.addImport("parse", roc_modules.parse);
|
|
cross_shim_lib.root_module.addImport("layout", roc_modules.layout);
|
|
cross_shim_lib.root_module.addImport("eval", roc_modules.eval);
|
|
cross_shim_lib.root_module.addImport("reporting", roc_modules.reporting);
|
|
cross_shim_lib.root_module.addImport("tracy", roc_modules.tracy);
|
|
cross_shim_lib.root_module.addImport("build_options", roc_modules.build_options);
|
|
// Note: ipc module is NOT added for wasm32-freestanding as it uses POSIX calls
|
|
// The interpreter shim main.zig has a stub for wasm32
|
|
} else {
|
|
roc_modules.addAll(cross_shim_lib);
|
|
}
|
|
cross_shim_lib.root_module.addImport("compiled_builtins", compiled_builtins_module);
|
|
cross_shim_lib.step.dependOn(&write_compiled_builtins.step);
|
|
cross_shim_lib.addObject(cross_builtins_obj);
|
|
cross_shim_lib.bundle_compiler_rt = true;
|
|
|
|
// Copy to target-specific directory for embedding
|
|
const copy_cross_shim = b.addUpdateSourceFiles();
|
|
copy_cross_shim.addCopyFileToSource(
|
|
cross_shim_lib.getEmittedBin(),
|
|
b.pathJoin(&.{ "src/cli/targets", cross_target.name, "libroc_interpreter_shim.a" }),
|
|
);
|
|
exe.step.dependOn(©_cross_shim.step);
|
|
}
|
|
|
|
const config = b.addOptions();
|
|
config.addOption(bool, "llvm", enable_llvm);
|
|
exe.root_module.addOptions("config", config);
|
|
exe.root_module.addAnonymousImport("legal_details", .{ .root_source_file = b.path("legal_details") });
|
|
|
|
if (enable_llvm) {
|
|
const llvm_paths = llvmPaths(b, target, use_system_llvm, user_llvm_path) orelse return null;
|
|
|
|
exe.addLibraryPath(.{ .cwd_relative = llvm_paths.lib });
|
|
exe.addIncludePath(.{ .cwd_relative = llvm_paths.include });
|
|
try addStaticLlvmOptionsToModule(exe.root_module);
|
|
}
|
|
|
|
add_tracy(b, roc_modules.build_options, exe, target, enable_llvm, tracy);
|
|
|
|
exe.linkLibrary(zstd.artifact("zstd"));
|
|
|
|
return exe;
|
|
}
|
|
|
|
fn install_and_run(
|
|
b: *std.Build,
|
|
no_bin: bool,
|
|
exe: *Step.Compile,
|
|
build_step: *Step,
|
|
run_step: *Step,
|
|
run_args: []const []const u8,
|
|
) void {
|
|
if (run_step != build_step) {
|
|
run_step.dependOn(build_step);
|
|
}
|
|
if (no_bin) {
|
|
// No build, just build, don't actually install or run.
|
|
build_step.dependOn(&exe.step);
|
|
b.getInstallStep().dependOn(&exe.step);
|
|
} else {
|
|
const install = b.addInstallArtifact(exe, .{});
|
|
|
|
// Add a step to print success message after build completes
|
|
const success_step = PrintBuildSuccessStep.create(b);
|
|
success_step.step.dependOn(&install.step);
|
|
build_step.dependOn(&success_step.step);
|
|
|
|
b.getInstallStep().dependOn(&install.step);
|
|
|
|
const run = b.addRunArtifact(exe);
|
|
run.step.dependOn(&install.step);
|
|
if (run_args.len != 0) {
|
|
run.addArgs(run_args);
|
|
}
|
|
run_step.dependOn(&run.step);
|
|
}
|
|
}
|
|
|
|
const ParsedBuildArgs = struct {
|
|
run_args: []const []const u8,
|
|
test_filters: []const []const u8,
|
|
};
|
|
|
|
fn appendFilter(
|
|
list: *std.ArrayList([]const u8),
|
|
b: *std.Build,
|
|
value: []const u8,
|
|
) void {
|
|
const trimmed = std.mem.trim(u8, value, " \t\n\r");
|
|
if (trimmed.len == 0) return;
|
|
list.append(b.allocator, b.dupe(trimmed)) catch @panic("OOM while parsing --test-filter value");
|
|
}
|
|
|
|
fn parseBuildArgs(b: *std.Build) ParsedBuildArgs {
|
|
const raw_args = b.args orelse return .{
|
|
.run_args = &.{},
|
|
.test_filters = &.{},
|
|
};
|
|
|
|
var run_args_list = std.ArrayList([]const u8).empty;
|
|
var filter_list = std.ArrayList([]const u8).empty;
|
|
|
|
var i: usize = 0;
|
|
while (i < raw_args.len) {
|
|
const arg = raw_args[i];
|
|
|
|
if (std.mem.eql(u8, arg, "--test-filter")) {
|
|
i += 1;
|
|
if (i >= raw_args.len) {
|
|
std.log.warn("ignoring --test-filter with no value", .{});
|
|
break;
|
|
}
|
|
const value = raw_args[i];
|
|
appendFilter(&filter_list, b, value);
|
|
i += 1;
|
|
continue;
|
|
}
|
|
|
|
if (std.mem.startsWith(u8, arg, "--test-filter=")) {
|
|
const value = arg["--test-filter=".len..];
|
|
appendFilter(&filter_list, b, value);
|
|
i += 1;
|
|
continue;
|
|
}
|
|
|
|
run_args_list.append(b.allocator, arg) catch @panic("OOM while recording build arguments");
|
|
i += 1;
|
|
}
|
|
|
|
const run_args = run_args_list.toOwnedSlice(b.allocator) catch @panic("OOM while finalizing build arguments");
|
|
const test_filters = filter_list.toOwnedSlice(b.allocator) catch @panic("OOM while finalizing test filters");
|
|
|
|
return .{ .run_args = run_args, .test_filters = test_filters };
|
|
}
|
|
|
|
fn add_tracy(
|
|
b: *std.Build,
|
|
module_build_options: *std.Build.Module,
|
|
base: *Step.Compile,
|
|
target: ResolvedTarget,
|
|
links_llvm: bool,
|
|
tracy: ?[]const u8,
|
|
) void {
|
|
base.root_module.addImport("build_options", module_build_options);
|
|
if (tracy) |tracy_path| {
|
|
const client_cpp = b.pathJoin(
|
|
&[_][]const u8{ tracy_path, "public", "TracyClient.cpp" },
|
|
);
|
|
|
|
// On mingw, we need to opt into windows 7+ to get some features required by tracy.
|
|
const tracy_c_flags: []const []const u8 = if (target.result.os.tag == .windows and target.result.abi == .gnu)
|
|
&[_][]const u8{ "-DTRACY_ENABLE=1", "-fno-sanitize=undefined", "-D_WIN32_WINNT=0x601" }
|
|
else
|
|
&[_][]const u8{ "-DTRACY_ENABLE=1", "-fno-sanitize=undefined" };
|
|
|
|
base.root_module.addIncludePath(.{ .cwd_relative = tracy_path });
|
|
base.root_module.addCSourceFile(.{ .file = .{ .cwd_relative = client_cpp }, .flags = tracy_c_flags });
|
|
base.root_module.addCSourceFile(.{ .file = .{ .cwd_relative = "src/build/tracy-shutdown.cpp" }, .flags = tracy_c_flags });
|
|
if (!links_llvm) {
|
|
base.root_module.linkSystemLibrary("c++", .{ .use_pkg_config = .no });
|
|
}
|
|
base.root_module.link_libc = true;
|
|
|
|
if (target.result.os.tag == .windows) {
|
|
base.root_module.linkSystemLibrary("dbghelp", .{});
|
|
base.root_module.linkSystemLibrary("ws2_32", .{});
|
|
}
|
|
}
|
|
}
|
|
|
|
const LlvmPaths = struct {
|
|
include: []const u8,
|
|
lib: []const u8,
|
|
};
|
|
|
|
// This functions is not used right now due to AFL requiring system llvm.
|
|
// This will be used once we begin linking roc to llvm.
|
|
fn llvmPaths(
|
|
b: *std.Build,
|
|
target: ResolvedTarget,
|
|
use_system_llvm: bool,
|
|
user_llvm_path: ?[]const u8,
|
|
) ?LlvmPaths {
|
|
if (use_system_llvm and user_llvm_path != null) {
|
|
std.log.err("-Dsystem-llvm and -Dllvm-path cannot both be specified", .{});
|
|
std.process.exit(1);
|
|
}
|
|
|
|
if (use_system_llvm) {
|
|
const llvm_config_path = b.findProgram(&.{"llvm-config"}, &.{""}) catch {
|
|
std.log.err("Failed to find system llvm-config binary", .{});
|
|
std.process.exit(1);
|
|
};
|
|
const llvm_lib_dir = std.mem.trimRight(u8, b.run(&.{ llvm_config_path, "--libdir" }), "\n");
|
|
const llvm_include_dir = std.mem.trimRight(u8, b.run(&.{ llvm_config_path, "--includedir" }), "\n");
|
|
|
|
return .{
|
|
.include = llvm_include_dir,
|
|
.lib = llvm_lib_dir,
|
|
};
|
|
}
|
|
|
|
if (user_llvm_path) |llvm_path| {
|
|
// We are just trust the user.
|
|
return .{
|
|
.include = b.pathJoin(&.{ llvm_path, "include" }),
|
|
.lib = b.pathJoin(&.{ llvm_path, "lib" }),
|
|
};
|
|
}
|
|
|
|
// No user specified llvm. Go download it from roc-bootstrap.
|
|
const raw_triple = target.result.linuxTriple(b.allocator) catch @panic("OOM");
|
|
if (!supported_deps_triples.has(raw_triple)) {
|
|
std.log.err("Target triple({s}) not supported by roc-bootstrap.\n", .{raw_triple});
|
|
std.log.err("Please specify the either `-Dsystem-llvm` or `-Dllvm-path`.\n", .{});
|
|
std.process.exit(1);
|
|
}
|
|
const triple = supported_deps_triples.get(raw_triple).?;
|
|
const deps_name = b.fmt("roc_deps_{s}", .{triple});
|
|
const deps = b.lazyDependency(deps_name, .{}) orelse return null;
|
|
const lazy_llvm_path = deps.path(".");
|
|
// TODO: Is this ok to do in the zig build system?
|
|
// We aren't in the make phase, but our static dep doesn't have a make phase anyway.
|
|
// Not sure how else to get a static path to the downloaded dependency.
|
|
const llvm_path = lazy_llvm_path.getPath(deps.builder);
|
|
return .{
|
|
.include = b.pathJoin(&.{ llvm_path, "include" }),
|
|
.lib = b.pathJoin(&.{ llvm_path, "lib" }),
|
|
};
|
|
}
|
|
|
|
const supported_deps_triples = std.StaticStringMap([]const u8).initComptime(.{
|
|
.{ "aarch64-macos-none", "aarch64_macos_none" },
|
|
.{ "aarch64-linux-musl", "aarch64_linux_musl" },
|
|
.{ "aarch64-windows-gnu", "aarch64_windows_gnu" },
|
|
.{ "arm-linux-musleabihf", "arm_linux_musleabihf" },
|
|
.{ "x86-linux-musl", "x86_linux_musl" },
|
|
.{ "x86_64-linux-musl", "x86_64_linux_musl" },
|
|
.{ "x86_64-macos-none", "x86_64_macos_none" },
|
|
.{ "x86_64-windows-gnu", "x86_64_windows_gnu" },
|
|
// We also support the gnu linux targets.
|
|
// For those, we just map to musl.
|
|
.{ "aarch64-linux-gnu", "aarch64_linux_musl" },
|
|
.{ "arm-linux-gnueabihf", "arm_linux_musleabihf" },
|
|
.{ "x86-linux-gnu", "x86_linux_musl" },
|
|
.{ "x86_64-linux-gnu", "x86_64_linux_musl" },
|
|
});
|
|
|
|
// The following is lifted from the zig compiler.
|
|
fn addStaticLlvmOptionsToModule(mod: *std.Build.Module) !void {
|
|
const cpp_cflags = exe_cflags ++ [_][]const u8{"-DNDEBUG=1"};
|
|
mod.addCSourceFiles(.{
|
|
.files = &cpp_sources,
|
|
.flags = &cpp_cflags,
|
|
});
|
|
|
|
const link_static = std.Build.Module.LinkSystemLibraryOptions{
|
|
.preferred_link_mode = .static,
|
|
.search_strategy = .mode_first,
|
|
};
|
|
for (lld_libs) |lib_name| {
|
|
mod.linkSystemLibrary(lib_name, link_static);
|
|
}
|
|
|
|
for (llvm_libs) |lib_name| {
|
|
mod.linkSystemLibrary(lib_name, link_static);
|
|
}
|
|
|
|
mod.linkSystemLibrary("z", link_static);
|
|
|
|
if (mod.resolved_target.?.result.os.tag != .windows or mod.resolved_target.?.result.abi != .msvc) {
|
|
// Use Zig's bundled static libc++ to keep the binary statically linked
|
|
mod.link_libcpp = true;
|
|
}
|
|
|
|
if (mod.resolved_target.?.result.os.tag == .windows) {
|
|
mod.linkSystemLibrary("ws2_32", .{});
|
|
mod.linkSystemLibrary("version", .{});
|
|
mod.linkSystemLibrary("uuid", .{});
|
|
mod.linkSystemLibrary("ole32", .{});
|
|
}
|
|
}
|
|
|
|
const cpp_sources = [_][]const u8{
|
|
"src/build/zig_llvm.cpp",
|
|
};
|
|
|
|
const exe_cflags = [_][]const u8{
|
|
"-std=c++17",
|
|
"-D__STDC_CONSTANT_MACROS",
|
|
"-D__STDC_FORMAT_MACROS",
|
|
"-D__STDC_LIMIT_MACROS",
|
|
"-D_GNU_SOURCE",
|
|
"-fno-exceptions",
|
|
"-fno-rtti",
|
|
"-fno-stack-protector",
|
|
"-fvisibility-inlines-hidden",
|
|
"-Wno-type-limits",
|
|
"-Wno-missing-braces",
|
|
"-Wno-comment",
|
|
};
|
|
const lld_libs = [_][]const u8{
|
|
"lldMinGW",
|
|
"lldELF",
|
|
"lldCOFF",
|
|
"lldWasm",
|
|
"lldMachO",
|
|
"lldCommon",
|
|
};
|
|
// This list can be re-generated with `llvm-config --libfiles` and then
|
|
// reformatting using your favorite text editor. Note we do not execute
|
|
// `llvm-config` here because we are cross compiling. Also omit LLVMTableGen
|
|
// from these libs.
|
|
const llvm_libs = [_][]const u8{
|
|
"LLVMWindowsManifest",
|
|
"LLVMXRay",
|
|
"LLVMLibDriver",
|
|
"LLVMDlltoolDriver",
|
|
"LLVMTextAPIBinaryReader",
|
|
"LLVMCoverage",
|
|
"LLVMLineEditor",
|
|
"LLVMXCoreDisassembler",
|
|
"LLVMXCoreCodeGen",
|
|
"LLVMXCoreDesc",
|
|
"LLVMXCoreInfo",
|
|
"LLVMX86TargetMCA",
|
|
"LLVMX86Disassembler",
|
|
"LLVMX86AsmParser",
|
|
"LLVMX86CodeGen",
|
|
"LLVMX86Desc",
|
|
"LLVMX86Info",
|
|
"LLVMWebAssemblyDisassembler",
|
|
"LLVMWebAssemblyAsmParser",
|
|
"LLVMWebAssemblyCodeGen",
|
|
"LLVMWebAssemblyUtils",
|
|
"LLVMWebAssemblyDesc",
|
|
"LLVMWebAssemblyInfo",
|
|
"LLVMVEDisassembler",
|
|
"LLVMVEAsmParser",
|
|
"LLVMVECodeGen",
|
|
"LLVMVEDesc",
|
|
"LLVMVEInfo",
|
|
"LLVMSystemZDisassembler",
|
|
"LLVMSystemZAsmParser",
|
|
"LLVMSystemZCodeGen",
|
|
"LLVMSystemZDesc",
|
|
"LLVMSystemZInfo",
|
|
"LLVMSparcDisassembler",
|
|
"LLVMSparcAsmParser",
|
|
"LLVMSparcCodeGen",
|
|
"LLVMSparcDesc",
|
|
"LLVMSparcInfo",
|
|
"LLVMRISCVTargetMCA",
|
|
"LLVMRISCVDisassembler",
|
|
"LLVMRISCVAsmParser",
|
|
"LLVMRISCVCodeGen",
|
|
"LLVMRISCVDesc",
|
|
"LLVMRISCVInfo",
|
|
"LLVMPowerPCDisassembler",
|
|
"LLVMPowerPCAsmParser",
|
|
"LLVMPowerPCCodeGen",
|
|
"LLVMPowerPCDesc",
|
|
"LLVMPowerPCInfo",
|
|
"LLVMNVPTXCodeGen",
|
|
"LLVMNVPTXDesc",
|
|
"LLVMNVPTXInfo",
|
|
"LLVMSPIRVAnalysis",
|
|
"LLVMSPIRVCodeGen",
|
|
"LLVMSPIRVDesc",
|
|
"LLVMSPIRVInfo",
|
|
"LLVMMSP430Disassembler",
|
|
"LLVMMSP430AsmParser",
|
|
"LLVMMSP430CodeGen",
|
|
"LLVMMSP430Desc",
|
|
"LLVMMSP430Info",
|
|
"LLVMMipsDisassembler",
|
|
"LLVMMipsAsmParser",
|
|
"LLVMMipsCodeGen",
|
|
"LLVMMipsDesc",
|
|
"LLVMMipsInfo",
|
|
"LLVMLoongArchDisassembler",
|
|
"LLVMLoongArchAsmParser",
|
|
"LLVMLoongArchCodeGen",
|
|
"LLVMLoongArchDesc",
|
|
"LLVMLoongArchInfo",
|
|
"LLVMLanaiDisassembler",
|
|
"LLVMLanaiCodeGen",
|
|
"LLVMLanaiAsmParser",
|
|
"LLVMLanaiDesc",
|
|
"LLVMLanaiInfo",
|
|
"LLVMHexagonDisassembler",
|
|
"LLVMHexagonCodeGen",
|
|
"LLVMHexagonAsmParser",
|
|
"LLVMHexagonDesc",
|
|
"LLVMHexagonInfo",
|
|
"LLVMBPFDisassembler",
|
|
"LLVMBPFAsmParser",
|
|
"LLVMBPFCodeGen",
|
|
"LLVMBPFDesc",
|
|
"LLVMBPFInfo",
|
|
"LLVMAVRDisassembler",
|
|
"LLVMAVRAsmParser",
|
|
"LLVMAVRCodeGen",
|
|
"LLVMAVRDesc",
|
|
"LLVMAVRInfo",
|
|
"LLVMARMDisassembler",
|
|
"LLVMARMAsmParser",
|
|
"LLVMARMCodeGen",
|
|
"LLVMARMDesc",
|
|
"LLVMARMUtils",
|
|
"LLVMARMInfo",
|
|
"LLVMAMDGPUTargetMCA",
|
|
"LLVMAMDGPUDisassembler",
|
|
"LLVMAMDGPUAsmParser",
|
|
"LLVMAMDGPUCodeGen",
|
|
"LLVMAMDGPUDesc",
|
|
"LLVMAMDGPUUtils",
|
|
"LLVMAMDGPUInfo",
|
|
"LLVMAArch64Disassembler",
|
|
"LLVMAArch64AsmParser",
|
|
"LLVMAArch64CodeGen",
|
|
"LLVMAArch64Desc",
|
|
"LLVMAArch64Utils",
|
|
"LLVMAArch64Info",
|
|
"LLVMOrcDebugging",
|
|
"LLVMOrcJIT",
|
|
"LLVMWindowsDriver",
|
|
"LLVMMCJIT",
|
|
"LLVMJITLink",
|
|
"LLVMInterpreter",
|
|
"LLVMExecutionEngine",
|
|
"LLVMRuntimeDyld",
|
|
"LLVMOrcTargetProcess",
|
|
"LLVMOrcShared",
|
|
"LLVMDWP",
|
|
"LLVMDebugInfoLogicalView",
|
|
"LLVMDebugInfoGSYM",
|
|
"LLVMOption",
|
|
"LLVMObjectYAML",
|
|
"LLVMObjCopy",
|
|
"LLVMMCA",
|
|
"LLVMMCDisassembler",
|
|
"LLVMLTO",
|
|
"LLVMPasses",
|
|
"LLVMCGData",
|
|
"LLVMHipStdPar",
|
|
"LLVMCFGuard",
|
|
"LLVMCoroutines",
|
|
"LLVMSandboxIR",
|
|
"LLVMipo",
|
|
"LLVMVectorize",
|
|
"LLVMLinker",
|
|
"LLVMInstrumentation",
|
|
"LLVMFrontendOpenMP",
|
|
"LLVMFrontendAtomic",
|
|
"LLVMFrontendOffloading",
|
|
"LLVMFrontendOpenACC",
|
|
"LLVMFrontendHLSL",
|
|
"LLVMFrontendDriver",
|
|
"LLVMExtensions",
|
|
"LLVMDWARFLinkerParallel",
|
|
"LLVMDWARFLinkerClassic",
|
|
"LLVMDWARFLinker",
|
|
"LLVMGlobalISel",
|
|
"LLVMMIRParser",
|
|
"LLVMAsmPrinter",
|
|
"LLVMSelectionDAG",
|
|
"LLVMCodeGen",
|
|
"LLVMTarget",
|
|
"LLVMObjCARCOpts",
|
|
"LLVMCodeGenTypes",
|
|
"LLVMIRPrinter",
|
|
"LLVMInterfaceStub",
|
|
"LLVMFileCheck",
|
|
"LLVMFuzzMutate",
|
|
"LLVMScalarOpts",
|
|
"LLVMInstCombine",
|
|
"LLVMAggressiveInstCombine",
|
|
"LLVMTransformUtils",
|
|
"LLVMBitWriter",
|
|
"LLVMAnalysis",
|
|
"LLVMProfileData",
|
|
"LLVMSymbolize",
|
|
"LLVMDebugInfoBTF",
|
|
"LLVMDebugInfoPDB",
|
|
"LLVMDebugInfoMSF",
|
|
"LLVMDebugInfoDWARF",
|
|
"LLVMObject",
|
|
"LLVMTextAPI",
|
|
"LLVMMCParser",
|
|
"LLVMIRReader",
|
|
"LLVMAsmParser",
|
|
"LLVMMC",
|
|
"LLVMDebugInfoCodeView",
|
|
"LLVMBitReader",
|
|
"LLVMFuzzerCLI",
|
|
"LLVMCore",
|
|
"LLVMRemarks",
|
|
"LLVMBitstreamReader",
|
|
"LLVMBinaryFormat",
|
|
"LLVMTargetParser",
|
|
"LLVMSupport",
|
|
"LLVMDemangle",
|
|
};
|
|
|
|
/// Get the compiler version string for cache versioning.
|
|
/// Returns a string like "debug-abc12345" where abc12345 is the git commit SHA.
|
|
/// If git is not available, falls back to "debug-no-git" format.
|
|
fn getCompilerVersion(b: *std.Build, optimize: OptimizeMode) []const u8 {
|
|
const build_mode = switch (optimize) {
|
|
.Debug => "debug",
|
|
.ReleaseSafe => "release-safe",
|
|
.ReleaseFast => "release-fast",
|
|
.ReleaseSmall => "release-small",
|
|
};
|
|
|
|
// Try to get git commit SHA using std.process.Child.run
|
|
const result = std.process.Child.run(.{
|
|
.allocator = b.allocator,
|
|
.argv = &[_][]const u8{ "git", "rev-parse", "--short=8", "HEAD" },
|
|
}) catch {
|
|
// Git command failed, use fallback
|
|
return std.fmt.allocPrint(b.allocator, "{s}-no-git", .{build_mode}) catch build_mode;
|
|
};
|
|
defer b.allocator.free(result.stdout);
|
|
defer b.allocator.free(result.stderr);
|
|
|
|
if (result.term == .Exited and result.term.Exited == 0) {
|
|
// Git succeeded, use the commit SHA
|
|
const commit_sha = std.mem.trim(u8, result.stdout, " \n\r\t");
|
|
if (commit_sha.len > 0) {
|
|
return std.fmt.allocPrint(b.allocator, "{s}-{s}", .{ build_mode, commit_sha }) catch
|
|
std.fmt.allocPrint(b.allocator, "{s}-no-git", .{build_mode}) catch build_mode;
|
|
}
|
|
}
|
|
|
|
// Git not available or failed, use fallback
|
|
return std.fmt.allocPrint(b.allocator, "{s}-no-git", .{build_mode}) catch build_mode;
|
|
}
|
|
|
|
/// Generate glibc stubs at build time for cross-compilation
|
|
///
|
|
/// This is a minimal implementation that generates essential symbols needed for basic
|
|
/// cross-compilation to glibc targets. It creates assembly stubs with required symbols
|
|
/// like __libc_start_main, abort, getauxval, and _IO_stdin_used.
|
|
///
|
|
/// Future work: Parse Zig's abilists file to generate comprehensive
|
|
/// symbol coverage with proper versioning (e.g., symbol@@GLIBC_2.17). The abilists
|
|
/// contains thousands of glibc symbols across different versions and architectures
|
|
/// that could provide more complete stub coverage for complex applications.
|
|
fn generateGlibcStub(b: *std.Build, target: ResolvedTarget, target_name: []const u8) ?*Step.UpdateSourceFiles {
|
|
|
|
// Generate assembly stub with comprehensive symbols using the new build module
|
|
var assembly_buf = std.ArrayList(u8).empty;
|
|
defer assembly_buf.deinit(b.allocator);
|
|
|
|
const writer = assembly_buf.writer(b.allocator);
|
|
const target_arch = target.result.cpu.arch;
|
|
|
|
glibc_stub_build.generateComprehensiveStub(writer, target_arch) catch |err| {
|
|
std.log.warn("Failed to generate comprehensive stub assembly for {s}: {}, using minimal ELF", .{ target_name, err });
|
|
// Fall back to minimal ELF
|
|
const stub_content = switch (target.result.cpu.arch) {
|
|
.aarch64 => createMinimalElfArm64(),
|
|
.x86_64 => createMinimalElfX64(),
|
|
else => return null,
|
|
};
|
|
|
|
const write_stub = b.addWriteFiles();
|
|
const libc_so_6 = write_stub.add("libc.so.6", stub_content);
|
|
const libc_so = write_stub.add("libc.so", stub_content);
|
|
|
|
const copy_stubs = b.addUpdateSourceFiles();
|
|
// Platforms that need glibc stubs
|
|
const glibc_platforms = [_][]const u8{ "int", "str" };
|
|
for (glibc_platforms) |platform| {
|
|
copy_stubs.addCopyFileToSource(libc_so_6, b.pathJoin(&.{ "test", platform, "platform/targets", target_name, "libc.so.6" }));
|
|
copy_stubs.addCopyFileToSource(libc_so, b.pathJoin(&.{ "test", platform, "platform/targets", target_name, "libc.so" }));
|
|
}
|
|
copy_stubs.step.dependOn(&write_stub.step);
|
|
|
|
return copy_stubs;
|
|
};
|
|
|
|
// Write the assembly file to the targets directory
|
|
const write_stub = b.addWriteFiles();
|
|
const asm_file = write_stub.add("libc_stub.s", assembly_buf.items);
|
|
|
|
// Compile the assembly into a proper shared library using Zig's build system
|
|
const libc_stub = glibc_stub_build.compileAssemblyStub(b, asm_file, target, .ReleaseSmall);
|
|
|
|
// Copy the generated files to all platforms that use glibc targets
|
|
const copy_stubs = b.addUpdateSourceFiles();
|
|
|
|
// Platforms that need glibc stubs (have glibc targets defined in their .roc files)
|
|
const glibc_platforms = [_][]const u8{ "int", "str" };
|
|
for (glibc_platforms) |platform| {
|
|
copy_stubs.addCopyFileToSource(libc_stub.getEmittedBin(), b.pathJoin(&.{ "test", platform, "platform/targets", target_name, "libc.so.6" }));
|
|
copy_stubs.addCopyFileToSource(libc_stub.getEmittedBin(), b.pathJoin(&.{ "test", platform, "platform/targets", target_name, "libc.so" }));
|
|
copy_stubs.addCopyFileToSource(asm_file, b.pathJoin(&.{ "test", platform, "platform/targets", target_name, "libc_stub.s" }));
|
|
}
|
|
copy_stubs.step.dependOn(&libc_stub.step);
|
|
copy_stubs.step.dependOn(&write_stub.step);
|
|
|
|
return copy_stubs;
|
|
}
|
|
|
|
/// Create a minimal ELF shared object for ARM64
|
|
fn createMinimalElfArm64() []const u8 {
|
|
// ARM64 minimal ELF shared object
|
|
return &[_]u8{
|
|
// ELF Header (64 bytes)
|
|
0x7F, 'E', 'L', 'F', // e_ident[EI_MAG0..3] - ELF magic
|
|
2, // e_ident[EI_CLASS] - ELFCLASS64
|
|
1, // e_ident[EI_DATA] - ELFDATA2LSB (little endian)
|
|
1, // e_ident[EI_VERSION] - EV_CURRENT
|
|
0, // e_ident[EI_OSABI] - ELFOSABI_NONE
|
|
0, // e_ident[EI_ABIVERSION]
|
|
0, 0, 0, 0, 0, 0, 0, // e_ident[EI_PAD] - padding
|
|
0x03, 0x00, // e_type - ET_DYN (shared object)
|
|
0xB7, 0x00, // e_machine - EM_AARCH64
|
|
0x01, 0x00, 0x00, 0x00, // e_version - EV_CURRENT
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // e_entry (not used for shared obj)
|
|
0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // e_phoff - program header offset
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // e_shoff - section header offset
|
|
0x00, 0x00, 0x00, 0x00, // e_flags
|
|
0x40, 0x00, // e_ehsize - ELF header size
|
|
0x38, 0x00, // e_phentsize - program header entry size
|
|
0x01, 0x00, // e_phnum - number of program headers
|
|
0x40, 0x00, // e_shentsize - section header entry size
|
|
0x00, 0x00, // e_shnum - number of section headers
|
|
0x00, 0x00, // e_shstrndx - section header string table index
|
|
|
|
// Program Header (56 bytes) - PT_LOAD
|
|
0x01, 0x00, 0x00, 0x00, // p_type - PT_LOAD
|
|
0x05, 0x00, 0x00, 0x00, // p_flags - PF_R | PF_X
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_offset
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_vaddr
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_paddr
|
|
0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_filesz
|
|
0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_memsz
|
|
0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_align
|
|
};
|
|
}
|
|
|
|
/// Create a minimal ELF shared object for x86-64
|
|
fn createMinimalElfX64() []const u8 {
|
|
// x86-64 minimal ELF shared object
|
|
return &[_]u8{
|
|
// ELF Header (64 bytes)
|
|
0x7F, 'E', 'L', 'F', // e_ident[EI_MAG0..3] - ELF magic
|
|
2, // e_ident[EI_CLASS] - ELFCLASS64
|
|
1, // e_ident[EI_DATA] - ELFDATA2LSB (little endian)
|
|
1, // e_ident[EI_VERSION] - EV_CURRENT
|
|
0, // e_ident[EI_OSABI] - ELFOSABI_NONE
|
|
0, // e_ident[EI_ABIVERSION]
|
|
0, 0, 0, 0, 0, 0, 0, // e_ident[EI_PAD] - padding
|
|
0x03, 0x00, // e_type - ET_DYN (shared object)
|
|
0x3E, 0x00, // e_machine - EM_X86_64
|
|
0x01, 0x00, 0x00, 0x00, // e_version - EV_CURRENT
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // e_entry (not used for shared obj)
|
|
0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // e_phoff - program header offset
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // e_shoff - section header offset
|
|
0x00, 0x00, 0x00, 0x00, // e_flags
|
|
0x40, 0x00, // e_ehsize - ELF header size
|
|
0x38, 0x00, // e_phentsize - program header entry size
|
|
0x01, 0x00, // e_phnum - number of program headers
|
|
0x40, 0x00, // e_shentsize - section header entry size
|
|
0x00, 0x00, // e_shnum - number of section headers
|
|
0x00, 0x00, // e_shstrndx - section header string table index
|
|
|
|
// Program Header (56 bytes) - PT_LOAD
|
|
0x01, 0x00, 0x00, 0x00, // p_type - PT_LOAD
|
|
0x05, 0x00, 0x00, 0x00, // p_flags - PF_R | PF_X
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_offset
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_vaddr
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_paddr
|
|
0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_filesz
|
|
0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_memsz
|
|
0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_align
|
|
};
|
|
}
|