/ build.zig
build.zig
  1  const std = @import("std");
  2  
  3  const Config = struct {
  4      pub const RiscvConfig = struct {
  5          /// Whether to depend on an SBI (Supervisor Binary Interface) implementation.
  6          /// This will assume that an SBI implementation is available and will be used to
  7          /// boot the kernel.
  8          sbi: bool = true,
  9  
 10          /// When `true`, the kernel assumes that at boot,
 11          /// `a0` will contain the boot hart id,
 12          /// and `a1` will contain a pointer to a valid FDT (DTB) structure.
 13          /// When `false`, provide the bytes for the FDT in the `fdt` field in the main `Config` struct.
 14          /// If `false`, and the `fdt` field is not provided, the kernel will be very limited
 15          /// in functionality.
 16          standard_args: bool = true,
 17      };
 18  
 19      /// RISC-V specific configuration.
 20      riscv: RiscvConfig = .{},
 21  
 22      /// The recommended way to set this is through the `dtb` build option.
 23      fdt: ?[]align(8) const u8 = null,
 24  };
 25  
 26  const TargetConfig = struct {
 27      target: []const u8,
 28      cpu: []const u8,
 29      linker_script: ?[]const u8 = null,
 30      dtb: ?[]const u8 = null,
 31      code_model: ?std.builtin.CodeModel = null,
 32      kernel_config: Config = .{},
 33      description: ?[]const u8 = null,
 34  };
 35  
 36  const NamedTargetConfig = struct {
 37      name: []const u8,
 38      config: TargetConfig,
 39  };
 40  
 41  fn discoverTargetConfigs(b: *std.Build) !std.ArrayList(NamedTargetConfig) {
 42      var configs: std.ArrayList(NamedTargetConfig) = .empty;
 43  
 44      var targets_dir = std.fs.cwd().openDir("targets", .{ .iterate = true }) catch |err| switch (err) {
 45          error.FileNotFound => {
 46              std.log.warn("targets/ directory not found, no target configs available", .{});
 47              return configs;
 48          },
 49          else => return err,
 50      };
 51      defer targets_dir.close();
 52  
 53      var iterator = targets_dir.iterate();
 54      while (try iterator.next()) |entry| {
 55          if (entry.kind != .file) continue;
 56          if (!std.mem.endsWith(u8, entry.name, ".zon")) continue;
 57  
 58          const target_name = entry.name[0 .. entry.name.len - 4];
 59  
 60          const config_content = targets_dir.readFileAllocOptions(
 61              b.allocator,
 62              entry.name,
 63              std.math.maxInt(usize),
 64              null,
 65              .of(u8),
 66              0,
 67          ) catch |err| {
 68              std.log.warn("Failed to read target config '{s}': {}", .{ entry.name, err });
 69              continue;
 70          };
 71  
 72          const target_config = std.zon.parse.fromSlice(TargetConfig, b.allocator, config_content, null, .{}) catch |err| {
 73              std.log.warn("Failed to parse target config '{s}': {}", .{ entry.name, err });
 74              continue;
 75          };
 76  
 77          try configs.append(b.allocator, .{ .name = try b.allocator.dupe(u8, target_name), .config = target_config });
 78      }
 79  
 80      return configs;
 81  }
 82  
 83  const ResolvedTargetConfig = struct {
 84      target: std.Build.ResolvedTarget,
 85      optimize: std.builtin.OptimizeMode,
 86      config: Config,
 87      linker_script: ?std.Build.LazyPath,
 88      code_model: std.builtin.CodeModel,
 89  };
 90  
 91  fn applyTargetConfig(b: *std.Build, target_config: TargetConfig) !ResolvedTargetConfig {
 92      const query = std.Target.Query.parse(.{
 93          .arch_os_abi = target_config.target,
 94          .cpu_features = target_config.cpu,
 95      }) catch |err| {
 96          std.log.err("Invalid target string '{s}': {}", .{ target_config.target, err });
 97          return err;
 98      };
 99  
100      const target = b.resolveTargetQuery(query);
101      const optimize = b.standardOptimizeOption(.{});
102  
103      var config = target_config.kernel_config;
104      if (target_config.dtb) |dtb_path| {
105          config.fdt = std.fs.cwd().readFileAllocOptions(
106              b.allocator,
107              dtb_path,
108              std.math.maxInt(usize),
109              null,
110              .@"8",
111              null,
112          ) catch |err| {
113              std.log.err("Failed to read DTB file '{s}': {}", .{ dtb_path, err });
114              return err;
115          };
116      }
117  
118      const linker_script = if (target_config.linker_script) |script_path|
119          b.path(script_path)
120      else
121          null;
122  
123      return .{
124          .target = target,
125          .optimize = optimize,
126          .config = config,
127          .linker_script = linker_script,
128          .code_model = target_config.code_model orelse .default,
129      };
130  }
131  
132  pub fn build(b: *std.Build) !void {
133      const discovered_configs = discoverTargetConfigs(b) catch |err| {
134          std.log.err("Failed to discover target configurations: {}", .{err});
135          return err;
136      };
137  
138      var help_text: std.ArrayList(u8) = .empty;
139      try help_text.appendSlice(b.allocator, "Available target configurations:");
140  
141      for (discovered_configs.items) |item| {
142          try help_text.appendSlice(b.allocator, "\n  ");
143          try help_text.appendSlice(b.allocator, item.name);
144          if (item.config.description) |desc| {
145              try help_text.appendSlice(b.allocator, " - ");
146              try help_text.appendSlice(b.allocator, desc);
147          }
148          try help_text.appendSlice(b.allocator, " (target: ");
149          try help_text.appendSlice(b.allocator, item.config.target);
150          try help_text.appendSlice(b.allocator, ")");
151      }
152      if (discovered_configs.items.len == 0) {
153          try help_text.appendSlice(b.allocator, "\n  none");
154      }
155  
156      const target_config_name = b.option([]const u8, "target-config", help_text.items);
157  
158      var build_params: ResolvedTargetConfig = undefined;
159  
160      if (target_config_name) |config_name| {
161          var found_config: ?TargetConfig = null;
162          for (discovered_configs.items) |item| {
163              if (std.mem.eql(u8, item.name, config_name)) {
164                  found_config = item.config;
165                  break;
166              }
167          }
168  
169          if (found_config) |target_config| {
170              build_params = try applyTargetConfig(b, target_config);
171              std.log.info("Using target configuration: {s}", .{config_name});
172          } else {
173              std.log.err("Unknown target configuration: {s}", .{config_name});
174              std.log.info("Available configurations:", .{});
175              for (discovered_configs.items) |item| {
176                  std.log.info("  {s}", .{item.name});
177              }
178              return error.UnknownConfig;
179          }
180      } else {
181          const target = b.standardTargetOptions(.{});
182          const optimize = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseSafe });
183  
184          const maybe_ld_path = b.option([]const u8, "ld-path", "Path to the linker script");
185          const maybe_code_model = b.option(std.builtin.CodeModel, "code-model", "Code model to use");
186          const maybe_dtb = b.option([]const u8, "dtb", "Path to the DTB file");
187  
188          var config_src: [:0]const u8 = ".{}";
189          config_src = std.fs.cwd().readFileAllocOptions(
190              b.allocator,
191              "config.zon",
192              std.math.maxInt(usize),
193              null,
194              .of(u8),
195              0,
196          ) catch |err| switch (err) {
197              error.FileNotFound => blk: {
198                  std.log.warn("config.zon not found, using default config", .{});
199                  break :blk config_src;
200              },
201              else => return err,
202          };
203  
204          var config = try std.zon.parse.fromSlice(Config, b.allocator, config_src, null, .{});
205  
206          if (maybe_dtb) |dtb| {
207              config.fdt = try std.fs.cwd().readFileAllocOptions(
208                  b.allocator,
209                  dtb,
210                  std.math.maxInt(usize),
211                  null,
212                  .@"8",
213                  null,
214              );
215          }
216  
217          build_params = .{
218              .target = target,
219              .optimize = optimize,
220              .config = config,
221              .linker_script = if (maybe_ld_path) |ld_path| b.path(ld_path) else null,
222              .code_model = maybe_code_model orelse .default,
223          };
224      }
225  
226      const mod = b.addModule("lightning", .{
227          .root_source_file = b.path("src/main.zig"),
228          .optimize = build_params.optimize,
229          .target = build_params.target,
230          .code_model = build_params.code_model,
231      });
232  
233      const opts = b.addOptions();
234  
235      opts.addOption(Config, "config", build_params.config);
236  
237      mod.addOptions("options", opts);
238  
239      const kernel = b.addExecutable(.{
240          .name = "lightning",
241          .root_module = mod,
242      });
243  
244      if (build_params.linker_script) |ld_path| {
245          kernel.setLinkerScript(ld_path);
246      }
247  
248      b.installArtifact(kernel);
249  }