diff --git a/build.zig b/build.zig index 2724a75..ddc3245 100644 --- a/build.zig +++ b/build.zig @@ -1,7 +1,16 @@ const std = @import("std"); -pub fn addExecutable(b: *std.Build, options: BuildStep.Options) *BuildStep { - return BuildStep.create(b, options); +pub fn addExecutable(b: *std.Build, options: GoBuildStep.Options) *GoBuildStep { + return GoBuildStep.create(b, options); +} + +/// Convenience method to create a library. +pub fn addLibrary(b: *std.Build, options: GoBuildStep.Options) *GoBuildStep { + var library_options = options; + library_options.make_library = true; + // A library build requires CGO. + library_options.cgo_enabled = true; + return GoBuildStep.create(b, library_options); } pub fn build(b: *std.Build) void { @@ -9,22 +18,25 @@ pub fn build(b: *std.Build) void { } /// Runs `go build` with relevant flags -pub const BuildStep = struct { +pub const GoBuildStep = struct { step: std.Build.Step, generated_bin: ?*std.Build.GeneratedFile, opts: Options, pub const Options = struct { + // The name of generated target. name: []const u8, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, package_path: std.Build.LazyPath, cgo_enabled: bool = true, + make_library: bool = false, + // TODO(rjk): Add Mac target sysroot support. }; /// Create a GoBuildStep - pub fn create(b: *std.Build, options: Options) *BuildStep { - const self = b.allocator.create(BuildStep) catch unreachable; + pub fn create(b: *std.Build, options: Options) *GoBuildStep { + const self = b.allocator.create(GoBuildStep) catch unreachable; self.* = .{ .opts = options, .generated_bin = null, @@ -32,14 +44,14 @@ pub const BuildStep = struct { .id = .custom, .name = "go build", .owner = b, - .makeFn = BuildStep.make, + .makeFn = GoBuildStep.make, }), }; return self; } - pub fn make(step: *std.Build.Step, progress: std.Progress.Node) !void { - const self: *BuildStep = @fieldParentPtr("step", step); + pub fn make(step: *std.Build.Step, mkoptions: std.Build.Step.MakeOptions) !void { + const self: *GoBuildStep = @fieldParentPtr("step", step); const b = step.owner; var go_args = std.ArrayList([]const u8).init(b.allocator); defer go_args.deinit(); @@ -59,22 +71,34 @@ pub const BuildStep = struct { var env = try std.process.getEnvMap(b.allocator); + // GOOS and GOARCH fields are not the same as triple fields on some + // platforms. Adjust them appropriately. + const target = self.opts.target; + const goarch = try isa_to_goarch(@tagName(target.result.cpu.arch)); + try env.put("GOARCH", goarch); + const goos = try ostag_to_goos(@tagName(target.result.os.tag)); + try env.put("GOOS", goos); + + // Cross compilling Go to MacOS requires providing a sysroot. The default + // sysroot exists when the target is macos, a MacOS SDK is installed and + // the Zig target is native. Otherwise, a sysroot *might* be present in + // some fashion. Add support for specifying the system root. + // CGO if (self.opts.cgo_enabled) { try env.put("CGO_ENABLED", "1"); // Set zig as the CGO compiler - const target = self.opts.target; + const ts = try self.mktargetstring(b); const cc = b.fmt( - "zig cc -target {s}-{s}-{s}", - .{ @tagName(target.result.cpu.arch), @tagName(target.result.os.tag), @tagName(target.result.abi) }, + "zig cc -target {s}", + .{ts}, ); try env.put("CC", cc); const cxx = b.fmt( - "zig c++ -target {s}-{s}-{s}", - .{ @tagName(target.result.cpu.arch), @tagName(target.result.os.tag), @tagName(target.result.abi) }, + "zig c++ -target {s}", + .{ts}, ); try env.put("CXX", cxx); - try env.put("GOOS", @tagName(target.result.os.tag)); // Tell the linker we are statically linking go_args.appendSlice(&.{ "--ldflags", "-linkmode=external -extldflags=-static" }) catch @panic("OOM"); @@ -82,14 +106,19 @@ pub const BuildStep = struct { try env.put("CGO_ENABLED", "0"); } - // Output file always needs to be added last + if (self.opts.make_library) { + // Where does the .h file go? (next to it) + try go_args.appendSlice(&.{"--buildmode=c-archive"}); + } + + // Package path always needs to be added last try go_args.append(self.opts.package_path.getPath(b)); const cmd = std.mem.join(b.allocator, " ", go_args.items) catch @panic("OOM"); - const node = progress.start(cmd, 1); + const node = mkoptions.progress_node.start(cmd, 1); defer node.end(); - // run the command + // Run the command try self.evalChildProcess(go_args.items, &env); if (self.generated_bin == null) { @@ -100,8 +129,13 @@ pub const BuildStep = struct { self.generated_bin.?.path = output_file; } + /// Return the LazyPath of the directory containing the Go-generated include file. + pub fn getIncludePath(self: *GoBuildStep) std.Build.LazyPath { + return self.getEmittedBin().dirname(); + } + /// Return the LazyPath of the generated binary - pub fn getEmittedBin(self: *BuildStep) std.Build.LazyPath { + pub fn getEmittedBin(self: *GoBuildStep) std.Build.LazyPath { if (self.generated_bin) |generated_bin| return .{ .generated = .{ .file = generated_bin } }; @@ -113,7 +147,7 @@ pub const BuildStep = struct { } /// Add a run step which depends on the GoBuildStep - pub fn addRunStep(self: *BuildStep) *std.Build.Step.Run { + pub fn addRunStep(self: *GoBuildStep) *std.Build.Step.Run { const b = self.step.owner; const run_step = std.Build.Step.Run.create(b, b.fmt("run {s}", .{self.opts.name})); run_step.step.dependOn(&self.step); @@ -123,8 +157,8 @@ pub const BuildStep = struct { return run_step; } - // Add an install step which depends on the GoBuildStep - pub fn addInstallStep(self: *BuildStep) void { + /// Add an install step which depends on the GoBuildStep + pub fn addInstallStep(self: *GoBuildStep) void { const b = self.step.owner; const bin_file = self.getEmittedBin(); const install_step = b.addInstallBinFile(bin_file, self.opts.name); @@ -132,7 +166,7 @@ pub const BuildStep = struct { b.getInstallStep().dependOn(&install_step.step); } - fn evalChildProcess(self: *BuildStep, argv: []const []const u8, env: *const std.process.EnvMap) !void { + fn evalChildProcess(self: *GoBuildStep, argv: []const []const u8, env: *const std.process.EnvMap) !void { const s = &self.step; const arena = s.owner.allocator; @@ -151,4 +185,61 @@ pub const BuildStep = struct { try std.Build.Step.handleChildProcessTerm(s, result.term, null, argv); } + + /// Create a target that can build the Go code. Uses "native" on MacOS + /// so that it has a sysroot. + // TODO(rjk): Add some kind of completed sysroot support. + fn mktargetstring(self: *GoBuildStep, b: *std.Build) ![]const u8 { + const target = self.opts.target; + if (target.result.os.tag == .ios or target.result.os.tag == .macos) { + return "native"; + } else { + return b.fmt( + "{s}-{s}-{s}", + .{ + @tagName(target.result.cpu.arch), + @tagName(target.result.os.tag), + @tagName(target.result.abi), + }, + ); + } + return error.NOTIMPL_CUSTOM_MACOS_SYSROOT; + } +}; + +const GoEnvError = error{ + UNSUPPORTED_GOARCH, + UNSUPPORTED_GOOS, +}; + +/// Convert a Zig arch triple member to the corresponding GOARCH value. +fn isa_to_goarch(isa: [:0]const u8) ![:0]const u8 { + if (std.mem.eql(u8, isa, "x86_64")) { + return "amd64"; + } else if (std.mem.eql(u8, isa, "arm")) { + return "arm"; + } else if (std.mem.eql(u8, isa, "aarch64")) { + return "arm64"; + } + // TODO(rjk): Consider adding some less popular processors that might + // work. + + return error.UNSUPPORTED_GOARCH; +} + +/// Convert a Zig os triple member to the corresponding GOOS value. +fn ostag_to_goos(os: [:0]const u8) ![:0]const u8 { + if (std.mem.startsWith(u8, os, "macos")) { + return "darwin"; + } else if (std.mem.startsWith(u8, os, "linux")) { + return "linux"; + } else if (std.mem.startsWith(u8, os, "windows")) { + return "windows"; + } + // TODO(rjk): Add less common OS. In particular, support Plan9. + return error.UNSUPPORTED_GOOS; +} + +const MacEnvError = error{ + NOTIMPL_CUSTOM_MACOS_SYSROOT, }; diff --git a/build.zig.zon b/build.zig.zon index f84e40a..d2b5305 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,7 @@ .{ - .name = "go", + .name = .go, .version = "0.0.0", .dependencies = .{}, .paths = .{""}, + .fingerprint = 0xb668935686b9cf3d, }