mirror of
https://github.com/ziglang/zig.git
synced 2026-02-12 20:37:54 +00:00
Merge pull request #17284 from ziglang/elf-tests
elf: link against musl libc, add ELF test harness, dynamically allocate misc SHF_ALLOC sections
This commit is contained in:
commit
7a43f45908
21
src/link.zig
21
src/link.zig
@ -465,9 +465,10 @@ pub const File = struct {
|
||||
.Exe => {},
|
||||
}
|
||||
switch (base.tag) {
|
||||
.coff, .elf, .macho, .plan9, .wasm => if (base.file) |f| {
|
||||
.elf => if (base.file) |f| {
|
||||
if (build_options.only_c) unreachable;
|
||||
if (base.intermediary_basename != null) {
|
||||
const use_lld = build_options.have_llvm and base.options.use_lld;
|
||||
if (base.intermediary_basename != null and use_lld) {
|
||||
// The file we have open is not the final file that we want to
|
||||
// make executable, so we don't have to close it.
|
||||
return;
|
||||
@ -480,6 +481,22 @@ pub const File = struct {
|
||||
.linux => std.os.ptrace(std.os.linux.PTRACE.DETACH, pid, 0, 0) catch |err| {
|
||||
log.warn("ptrace failure: {s}", .{@errorName(err)});
|
||||
},
|
||||
else => return error.HotSwapUnavailableOnHostOperatingSystem,
|
||||
}
|
||||
}
|
||||
},
|
||||
.coff, .macho, .plan9, .wasm => if (base.file) |f| {
|
||||
if (build_options.only_c) unreachable;
|
||||
if (base.intermediary_basename != null) {
|
||||
// The file we have open is not the final file that we want to
|
||||
// make executable, so we don't have to close it.
|
||||
return;
|
||||
}
|
||||
f.close();
|
||||
base.file = null;
|
||||
|
||||
if (base.child_pid) |pid| {
|
||||
switch (builtin.os.tag) {
|
||||
.macos => base.cast(MachO).?.ptraceDetach(pid) catch |err| {
|
||||
log.warn("detaching failed with error: {s}", .{@errorName(err)});
|
||||
},
|
||||
|
||||
113
src/link/Elf.zig
113
src/link/Elf.zig
@ -254,7 +254,7 @@ pub fn createEmpty(gpa: Allocator, options: link.Options) !*Elf {
|
||||
.default_sym_version = default_sym_version,
|
||||
};
|
||||
const use_llvm = options.use_llvm;
|
||||
if (use_llvm) {
|
||||
if (use_llvm and options.module != null) {
|
||||
self.llvm_object = try LlvmObject.create(gpa, options);
|
||||
}
|
||||
|
||||
@ -409,16 +409,24 @@ fn findFreeSpace(self: *Elf, object_size: u64, min_alignment: u64) u64 {
|
||||
}
|
||||
|
||||
const AllocateSegmentOpts = struct {
|
||||
addr: u64, // TODO find free VM space
|
||||
size: u64,
|
||||
alignment: u64,
|
||||
addr: ?u64 = null, // TODO find free VM space
|
||||
flags: u32 = elf.PF_R,
|
||||
};
|
||||
|
||||
fn allocateSegment(self: *Elf, opts: AllocateSegmentOpts) error{OutOfMemory}!u16 {
|
||||
pub fn allocateSegment(self: *Elf, opts: AllocateSegmentOpts) error{OutOfMemory}!u16 {
|
||||
const index = @as(u16, @intCast(self.phdrs.items.len));
|
||||
try self.phdrs.ensureUnusedCapacity(self.base.allocator, 1);
|
||||
const off = self.findFreeSpace(opts.size, opts.alignment);
|
||||
// Memory is always allocated in sequence.
|
||||
// TODO is this correct? Or should we implement something similar to `findFreeSpace`?
|
||||
// How would that impact HCS?
|
||||
const addr = opts.addr orelse blk: {
|
||||
assert(self.phdr_table_load_index != null);
|
||||
const phdr = &self.phdrs.items[index - 1];
|
||||
break :blk mem.alignForward(u64, phdr.p_vaddr + phdr.p_memsz, opts.alignment);
|
||||
};
|
||||
log.debug("allocating phdr({d})({c}{c}{c}) from 0x{x} to 0x{x} (0x{x} - 0x{x})", .{
|
||||
index,
|
||||
if (opts.flags & elf.PF_R != 0) @as(u8, 'R') else '_',
|
||||
@ -426,15 +434,15 @@ fn allocateSegment(self: *Elf, opts: AllocateSegmentOpts) error{OutOfMemory}!u16
|
||||
if (opts.flags & elf.PF_X != 0) @as(u8, 'X') else '_',
|
||||
off,
|
||||
off + opts.size,
|
||||
opts.addr,
|
||||
opts.addr + opts.size,
|
||||
addr,
|
||||
addr + opts.size,
|
||||
});
|
||||
self.phdrs.appendAssumeCapacity(.{
|
||||
.p_type = elf.PT_LOAD,
|
||||
.p_offset = off,
|
||||
.p_filesz = opts.size,
|
||||
.p_vaddr = opts.addr,
|
||||
.p_paddr = opts.addr,
|
||||
.p_vaddr = addr,
|
||||
.p_paddr = addr,
|
||||
.p_memsz = opts.size,
|
||||
.p_align = opts.alignment,
|
||||
.p_flags = opts.flags,
|
||||
@ -446,12 +454,12 @@ fn allocateSegment(self: *Elf, opts: AllocateSegmentOpts) error{OutOfMemory}!u16
|
||||
const AllocateAllocSectionOpts = struct {
|
||||
name: [:0]const u8,
|
||||
phdr_index: u16,
|
||||
alignment: u16 = 1,
|
||||
flags: u16 = elf.SHF_ALLOC,
|
||||
alignment: u64 = 1,
|
||||
flags: u64 = elf.SHF_ALLOC,
|
||||
type: u32 = elf.SHT_PROGBITS,
|
||||
};
|
||||
|
||||
fn allocateAllocSection(self: *Elf, opts: AllocateAllocSectionOpts) error{OutOfMemory}!u16 {
|
||||
pub fn allocateAllocSection(self: *Elf, opts: AllocateAllocSectionOpts) error{OutOfMemory}!u16 {
|
||||
const gpa = self.base.allocator;
|
||||
const phdr = &self.phdrs.items[opts.phdr_index];
|
||||
const index = @as(u16, @intCast(self.shdrs.items.len));
|
||||
@ -622,6 +630,7 @@ pub fn populateMissingMetadata(self: *Elf) !void {
|
||||
});
|
||||
const phdr = &self.phdrs.items[self.phdr_load_zerofill_index.?];
|
||||
phdr.p_offset = self.phdrs.items[self.phdr_load_rw_index.?].p_offset; // .bss overlaps .data
|
||||
phdr.p_memsz = 1024;
|
||||
}
|
||||
|
||||
if (self.shstrtab_section_index == null) {
|
||||
@ -965,6 +974,7 @@ pub fn flushModule(self: *Elf, comp: *Compilation, prog_node: *std.Progress.Node
|
||||
defer arena_allocator.deinit();
|
||||
const arena = arena_allocator.allocator();
|
||||
|
||||
const target = self.base.options.target;
|
||||
const directory = self.base.options.emit.?.directory; // Just an alias to make it shorter to type.
|
||||
const full_out_path = try directory.join(arena, &[_][]const u8{self.base.options.emit.?.sub_path});
|
||||
|
||||
@ -993,19 +1003,77 @@ pub fn flushModule(self: *Elf, comp: *Compilation, prog_node: *std.Progress.Node
|
||||
try positionals.append(.{ .path = key.status.success.object_path });
|
||||
}
|
||||
|
||||
// csu prelude
|
||||
var csu = try CsuObjects.init(arena, self.base.options, comp);
|
||||
if (csu.crt0) |v| try positionals.append(.{ .path = v });
|
||||
if (csu.crti) |v| try positionals.append(.{ .path = v });
|
||||
if (csu.crtbegin) |v| try positionals.append(.{ .path = v });
|
||||
|
||||
for (positionals.items) |obj| {
|
||||
const in_file = try std.fs.cwd().openFile(obj.path, .{});
|
||||
defer in_file.close();
|
||||
var parse_ctx: ParseErrorCtx = .{ .detected_cpu_arch = undefined };
|
||||
self.parsePositional(in_file, obj.path, obj.must_link, &parse_ctx) catch |err|
|
||||
try self.handleAndReportParseError(obj.path, err, &parse_ctx);
|
||||
}
|
||||
|
||||
var system_libs = std.ArrayList(SystemLib).init(arena);
|
||||
|
||||
// libc dep
|
||||
self.error_flags.missing_libc = false;
|
||||
if (self.base.options.link_libc) {
|
||||
if (self.base.options.libc_installation != null) {
|
||||
@panic("TODO explicit libc_installation");
|
||||
} else if (target.isGnuLibC()) {
|
||||
try system_libs.ensureUnusedCapacity(glibc.libs.len + 1);
|
||||
for (glibc.libs) |lib| {
|
||||
const lib_path = try std.fmt.allocPrint(arena, "{s}{c}lib{s}.so.{d}", .{
|
||||
comp.glibc_so_files.?.dir_path, fs.path.sep, lib.name, lib.sover,
|
||||
});
|
||||
system_libs.appendAssumeCapacity(.{ .path = lib_path });
|
||||
}
|
||||
system_libs.appendAssumeCapacity(.{
|
||||
.path = try comp.get_libc_crt_file(arena, "libc_nonshared.a"),
|
||||
});
|
||||
} else if (target.isMusl()) {
|
||||
const path = try comp.get_libc_crt_file(arena, switch (self.base.options.link_mode) {
|
||||
.Static => "libc.a",
|
||||
.Dynamic => "libc.so",
|
||||
});
|
||||
try system_libs.append(.{ .path = path });
|
||||
} else {
|
||||
self.error_flags.missing_libc = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (system_libs.items) |lib| {
|
||||
const in_file = try std.fs.cwd().openFile(lib.path, .{});
|
||||
defer in_file.close();
|
||||
var parse_ctx: ParseErrorCtx = .{ .detected_cpu_arch = undefined };
|
||||
self.parseLibrary(in_file, lib, false, &parse_ctx) catch |err|
|
||||
try self.handleAndReportParseError(lib.path, err, &parse_ctx);
|
||||
}
|
||||
|
||||
// Finally, as the last input objects we add compiler_rt and CSU postlude (if any).
|
||||
positionals.clearRetainingCapacity();
|
||||
|
||||
// compiler-rt. Since compiler_rt exports symbols like `memset`, it needs
|
||||
// to be after the shared libraries, so they are picked up from the shared
|
||||
// libraries, not libcompiler_rt.
|
||||
const compiler_rt_path: ?[]const u8 = blk: {
|
||||
if (comp.compiler_rt_lib) |x| break :blk x.full_object_path;
|
||||
if (comp.compiler_rt_obj) |x| break :blk x.full_object_path;
|
||||
break :blk null;
|
||||
};
|
||||
if (compiler_rt_path) |path| {
|
||||
try positionals.append(.{ .path = path });
|
||||
}
|
||||
if (compiler_rt_path) |path| try positionals.append(.{ .path = path });
|
||||
|
||||
// csu postlude
|
||||
if (csu.crtend) |v| try positionals.append(.{ .path = v });
|
||||
if (csu.crtn) |v| try positionals.append(.{ .path = v });
|
||||
|
||||
for (positionals.items) |obj| {
|
||||
const in_file = try std.fs.cwd().openFile(obj.path, .{});
|
||||
defer in_file.close();
|
||||
|
||||
var parse_ctx: ParseErrorCtx = .{ .detected_cpu_arch = undefined };
|
||||
self.parsePositional(in_file, obj.path, obj.must_link, &parse_ctx) catch |err|
|
||||
try self.handleAndReportParseError(obj.path, err, &parse_ctx);
|
||||
@ -1347,28 +1415,22 @@ fn parsePositional(
|
||||
if (Object.isObject(in_file)) {
|
||||
try self.parseObject(in_file, path, ctx);
|
||||
} else {
|
||||
try self.parseLibrary(in_file, path, .{
|
||||
.path = null,
|
||||
.needed = false,
|
||||
.weak = false,
|
||||
}, must_link, ctx);
|
||||
try self.parseLibrary(in_file, .{ .path = path }, must_link, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn parseLibrary(
|
||||
self: *Elf,
|
||||
in_file: std.fs.File,
|
||||
path: []const u8,
|
||||
lib: link.SystemLib,
|
||||
lib: SystemLib,
|
||||
must_link: bool,
|
||||
ctx: *ParseErrorCtx,
|
||||
) ParseError!void {
|
||||
const tracy = trace(@src());
|
||||
defer tracy.end();
|
||||
_ = lib;
|
||||
|
||||
if (Archive.isArchive(in_file)) {
|
||||
try self.parseArchive(in_file, path, must_link, ctx);
|
||||
try self.parseArchive(in_file, lib.path, must_link, ctx);
|
||||
} else return error.UnknownFileType;
|
||||
}
|
||||
|
||||
@ -4150,6 +4212,11 @@ pub const null_sym = elf.Elf64_Sym{
|
||||
.st_size = 0,
|
||||
};
|
||||
|
||||
const SystemLib = struct {
|
||||
needed: bool = false,
|
||||
path: []const u8,
|
||||
};
|
||||
|
||||
const std = @import("std");
|
||||
const build_options = @import("build_options");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
@ -136,7 +136,7 @@ fn initAtoms(self: *Object, elf_file: *Elf) !void {
|
||||
try self.comdat_groups.append(elf_file.base.allocator, comdat_group_index);
|
||||
},
|
||||
|
||||
elf.SHT_SYMTAB_SHNDX => @panic("TODO"),
|
||||
elf.SHT_SYMTAB_SHNDX => @panic("TODO SHT_SYMTAB_SHNDX"),
|
||||
|
||||
elf.SHT_NULL,
|
||||
elf.SHT_REL,
|
||||
@ -166,14 +166,20 @@ fn initAtoms(self: *Object, elf_file: *Elf) !void {
|
||||
};
|
||||
}
|
||||
|
||||
fn addAtom(self: *Object, shdr: elf.Elf64_Shdr, shndx: u16, name: [:0]const u8, elf_file: *Elf) !void {
|
||||
fn addAtom(
|
||||
self: *Object,
|
||||
shdr: elf.Elf64_Shdr,
|
||||
shndx: u16,
|
||||
name: [:0]const u8,
|
||||
elf_file: *Elf,
|
||||
) error{ OutOfMemory, Overflow }!void {
|
||||
const atom_index = try elf_file.addAtom();
|
||||
const atom = elf_file.atom(atom_index).?;
|
||||
atom.atom_index = atom_index;
|
||||
atom.name_offset = try elf_file.strtab.insert(elf_file.base.allocator, name);
|
||||
atom.file_index = self.index;
|
||||
atom.input_section_index = shndx;
|
||||
atom.output_section_index = self.getOutputSectionIndex(elf_file, shdr);
|
||||
atom.output_section_index = try self.getOutputSectionIndex(elf_file, shdr);
|
||||
atom.alive = true;
|
||||
self.atoms.items[shndx] = atom_index;
|
||||
|
||||
@ -188,7 +194,7 @@ fn addAtom(self: *Object, shdr: elf.Elf64_Shdr, shndx: u16, name: [:0]const u8,
|
||||
}
|
||||
}
|
||||
|
||||
fn getOutputSectionIndex(self: *Object, elf_file: *Elf, shdr: elf.Elf64_Shdr) u16 {
|
||||
fn getOutputSectionIndex(self: *Object, elf_file: *Elf, shdr: elf.Elf64_Shdr) error{OutOfMemory}!u16 {
|
||||
const name = blk: {
|
||||
const name = self.strings.getAssumeExists(shdr.sh_name);
|
||||
// if (shdr.sh_flags & elf.SHF_MERGE != 0) break :blk name;
|
||||
@ -223,10 +229,32 @@ fn getOutputSectionIndex(self: *Object, elf_file: *Elf, shdr: elf.Elf64_Shdr) u1
|
||||
else => flags,
|
||||
};
|
||||
};
|
||||
_ = flags;
|
||||
const out_shndx = elf_file.sectionByName(name) orelse {
|
||||
log.err("{}: output section {s} not found", .{ self.fmtPath(), name });
|
||||
@panic("TODO: missing output section!");
|
||||
const out_shndx = elf_file.sectionByName(name) orelse blk: {
|
||||
const is_alloc = flags & elf.SHF_ALLOC != 0;
|
||||
const is_write = flags & elf.SHF_WRITE != 0;
|
||||
const is_exec = flags & elf.SHF_EXECINSTR != 0;
|
||||
const is_tls = flags & elf.SHF_TLS != 0;
|
||||
if (!is_alloc or is_tls) {
|
||||
log.err("{}: output section {s} not found", .{ self.fmtPath(), name });
|
||||
@panic("TODO: missing output section!");
|
||||
}
|
||||
var phdr_flags: u32 = elf.PF_R;
|
||||
if (is_write) phdr_flags |= elf.PF_W;
|
||||
if (is_exec) phdr_flags |= elf.PF_X;
|
||||
const phdr_index = try elf_file.allocateSegment(.{
|
||||
.size = Elf.padToIdeal(shdr.sh_size),
|
||||
.alignment = if (is_tls) shdr.sh_addralign else elf_file.page_size,
|
||||
.flags = phdr_flags,
|
||||
});
|
||||
const shndx = try elf_file.allocateAllocSection(.{
|
||||
.name = name,
|
||||
.phdr_index = phdr_index,
|
||||
.alignment = shdr.sh_addralign,
|
||||
.flags = flags,
|
||||
.type = @"type",
|
||||
});
|
||||
try elf_file.last_atom_and_free_list_table.putNoClobber(elf_file.base.allocator, shndx, .{});
|
||||
break :blk shndx;
|
||||
};
|
||||
return out_shndx;
|
||||
}
|
||||
|
||||
@ -29,6 +29,12 @@ pub const cases = [_]Case{
|
||||
.import = @import("link/glibc_compat/build.zig"),
|
||||
},
|
||||
|
||||
// Elf Cases
|
||||
.{
|
||||
.build_root = "test/link",
|
||||
.import = @import("link/elf.zig"),
|
||||
},
|
||||
|
||||
// WASM Cases
|
||||
// https://github.com/ziglang/zig/issues/16938
|
||||
//.{
|
||||
|
||||
151
test/link/elf.zig
Normal file
151
test/link/elf.zig
Normal file
@ -0,0 +1,151 @@
|
||||
//! Here we test our ELF linker for correctness and functionality.
|
||||
//! Currently, we support linking x86_64 Linux, but in the future we
|
||||
//! will progressively relax those to exercise more combinations.
|
||||
|
||||
pub fn build(b: *Build) void {
|
||||
const elf_step = b.step("test-elf", "Run ELF tests");
|
||||
b.default_step = elf_step;
|
||||
|
||||
const musl_target = CrossTarget{
|
||||
.cpu_arch = .x86_64, // TODO relax this once ELF linker is able to handle other archs
|
||||
.os_tag = .linux,
|
||||
.abi = .musl,
|
||||
};
|
||||
|
||||
// Exercise linker with self-hosted backend (no LLVM)
|
||||
elf_step.dependOn(testLinkingZig(b, .{ .use_llvm = false }));
|
||||
|
||||
// Exercise linker with LLVM backend
|
||||
elf_step.dependOn(testEmptyObject(b, .{ .target = musl_target }));
|
||||
elf_step.dependOn(testLinkingC(b, .{ .target = musl_target }));
|
||||
elf_step.dependOn(testLinkingZig(b, .{}));
|
||||
}
|
||||
|
||||
fn testEmptyObject(b: *Build, opts: Options) *Step {
|
||||
const test_step = addTestStep(b, "empty-object", opts);
|
||||
|
||||
const exe = addExecutable(b, opts);
|
||||
addCSourceBytes(exe, "int main() { return 0; }");
|
||||
addCSourceBytes(exe, "");
|
||||
exe.is_linking_libc = true;
|
||||
|
||||
const run = addRunArtifact(exe);
|
||||
run.expectExitCode(0);
|
||||
test_step.dependOn(&run.step);
|
||||
|
||||
return test_step;
|
||||
}
|
||||
|
||||
fn testLinkingC(b: *Build, opts: Options) *Step {
|
||||
const test_step = addTestStep(b, "linking-c-static", opts);
|
||||
|
||||
const exe = addExecutable(b, opts);
|
||||
addCSourceBytes(exe,
|
||||
\\#include <stdio.h>
|
||||
\\int main() {
|
||||
\\ printf("Hello World!\n");
|
||||
\\ return 0;
|
||||
\\}
|
||||
);
|
||||
exe.is_linking_libc = true;
|
||||
|
||||
const run = addRunArtifact(exe);
|
||||
run.expectStdOutEqual("Hello World!\n");
|
||||
test_step.dependOn(&run.step);
|
||||
|
||||
const check = exe.checkObject();
|
||||
check.checkStart();
|
||||
check.checkExact("header");
|
||||
check.checkExact("type EXEC");
|
||||
check.checkStart();
|
||||
check.checkExact("section headers");
|
||||
check.checkNotPresent("name .dynamic");
|
||||
test_step.dependOn(&check.step);
|
||||
|
||||
return test_step;
|
||||
}
|
||||
|
||||
fn testLinkingZig(b: *Build, opts: Options) *Step {
|
||||
const test_step = addTestStep(b, "linking-zig-static", opts);
|
||||
|
||||
const exe = addExecutable(b, opts);
|
||||
addZigSourceBytes(exe,
|
||||
\\pub fn main() void {
|
||||
\\ @import("std").debug.print("Hello World!\n", .{});
|
||||
\\}
|
||||
);
|
||||
|
||||
const run = addRunArtifact(exe);
|
||||
run.expectStdErrEqual("Hello World!\n");
|
||||
test_step.dependOn(&run.step);
|
||||
|
||||
const check = exe.checkObject();
|
||||
check.checkStart();
|
||||
check.checkExact("header");
|
||||
check.checkExact("type EXEC");
|
||||
check.checkStart();
|
||||
check.checkExact("section headers");
|
||||
check.checkNotPresent("name .dynamic");
|
||||
test_step.dependOn(&check.step);
|
||||
|
||||
return test_step;
|
||||
}
|
||||
|
||||
const Options = struct {
|
||||
target: CrossTarget = .{ .cpu_arch = .x86_64, .os_tag = .linux },
|
||||
optimize: std.builtin.OptimizeMode = .Debug,
|
||||
use_llvm: bool = true,
|
||||
};
|
||||
|
||||
fn addTestStep(b: *Build, comptime prefix: []const u8, opts: Options) *Step {
|
||||
const target = opts.target.zigTriple(b.allocator) catch @panic("OOM");
|
||||
const optimize = @tagName(opts.optimize);
|
||||
const use_llvm = if (opts.use_llvm) "llvm" else "no-llvm";
|
||||
const name = std.fmt.allocPrint(b.allocator, "test-elf-" ++ prefix ++ "-{s}-{s}-{s}", .{
|
||||
target,
|
||||
optimize,
|
||||
use_llvm,
|
||||
}) catch @panic("OOM");
|
||||
return b.step(name, "");
|
||||
}
|
||||
|
||||
fn addExecutable(b: *Build, opts: Options) *Compile {
|
||||
return b.addExecutable(.{
|
||||
.name = "test",
|
||||
.target = opts.target,
|
||||
.optimize = opts.optimize,
|
||||
.single_threaded = true, // TODO temp until we teach linker how to handle TLS
|
||||
.use_llvm = opts.use_llvm,
|
||||
.use_lld = false,
|
||||
});
|
||||
}
|
||||
|
||||
fn addRunArtifact(comp: *Compile) *Run {
|
||||
const b = comp.step.owner;
|
||||
const run = b.addRunArtifact(comp);
|
||||
run.skip_foreign_checks = true;
|
||||
return run;
|
||||
}
|
||||
|
||||
fn addZigSourceBytes(comp: *Compile, bytes: []const u8) void {
|
||||
const b = comp.step.owner;
|
||||
const file = WriteFile.create(b).add("a.zig", bytes);
|
||||
file.addStepDependencies(&comp.step);
|
||||
comp.root_src = file;
|
||||
}
|
||||
|
||||
fn addCSourceBytes(comp: *Compile, bytes: []const u8) void {
|
||||
const b = comp.step.owner;
|
||||
const file = WriteFile.create(b).add("a.c", bytes);
|
||||
comp.addCSourceFile(.{ .file = file, .flags = &.{} });
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const Build = std.Build;
|
||||
const Compile = Step.Compile;
|
||||
const CrossTarget = std.zig.CrossTarget;
|
||||
const LazyPath = Build.LazyPath;
|
||||
const Run = Step.Run;
|
||||
const Step = Build.Step;
|
||||
const WriteFile = Step.WriteFile;
|
||||
Loading…
x
Reference in New Issue
Block a user