diff options
Diffstat (limited to 'lib/std/Build.zig')
| -rw-r--r-- | lib/std/Build.zig | 1780 |
1 files changed, 1780 insertions, 0 deletions
diff --git a/lib/std/Build.zig b/lib/std/Build.zig new file mode 100644 index 0000000000..d695637fc3 --- /dev/null +++ b/lib/std/Build.zig @@ -0,0 +1,1780 @@ +const std = @import("std.zig"); +const builtin = @import("builtin"); +const io = std.io; +const fs = std.fs; +const mem = std.mem; +const debug = std.debug; +const panic = std.debug.panic; +const assert = debug.assert; +const log = std.log; +const ArrayList = std.ArrayList; +const StringHashMap = std.StringHashMap; +const Allocator = mem.Allocator; +const process = std.process; +const EnvMap = std.process.EnvMap; +const fmt_lib = std.fmt; +const File = std.fs.File; +const CrossTarget = std.zig.CrossTarget; +const NativeTargetInfo = std.zig.system.NativeTargetInfo; +const Sha256 = std.crypto.hash.sha2.Sha256; +const Build = @This(); + +/// deprecated: use `CompileStep`. +pub const LibExeObjStep = CompileStep; +/// deprecated: use `Build`. +pub const Builder = Build; +/// deprecated: use `InstallDirStep.Options` +pub const InstallDirectoryOptions = InstallDirStep.Options; + +pub const Step = @import("Build/Step.zig"); +pub const CheckFileStep = @import("Build/CheckFileStep.zig"); +pub const CheckObjectStep = @import("Build/CheckObjectStep.zig"); +pub const ConfigHeaderStep = @import("Build/ConfigHeaderStep.zig"); +pub const EmulatableRunStep = @import("Build/EmulatableRunStep.zig"); +pub const FmtStep = @import("Build/FmtStep.zig"); +pub const InstallArtifactStep = @import("Build/InstallArtifactStep.zig"); +pub const InstallDirStep = @import("Build/InstallDirStep.zig"); +pub const InstallFileStep = @import("Build/InstallFileStep.zig"); +pub const InstallRawStep = @import("Build/InstallRawStep.zig"); +pub const CompileStep = @import("Build/CompileStep.zig"); +pub const LogStep = @import("Build/LogStep.zig"); +pub const OptionsStep = @import("Build/OptionsStep.zig"); +pub const RemoveDirStep = @import("Build/RemoveDirStep.zig"); +pub const RunStep = @import("Build/RunStep.zig"); +pub const TranslateCStep = @import("Build/TranslateCStep.zig"); +pub const WriteFileStep = @import("Build/WriteFileStep.zig"); + +install_tls: TopLevelStep, +uninstall_tls: TopLevelStep, +allocator: Allocator, +user_input_options: UserInputOptionsMap, +available_options_map: AvailableOptionsMap, +available_options_list: ArrayList(AvailableOption), +verbose: bool, +verbose_link: bool, +verbose_cc: bool, +verbose_air: bool, +verbose_llvm_ir: bool, +verbose_cimport: bool, +verbose_llvm_cpu_features: bool, +/// The purpose of executing the command is for a human to read compile errors from the terminal +prominent_compile_errors: bool, +color: enum { auto, on, off } = .auto, +reference_trace: ?u32 = null, +invalid_user_input: bool, +zig_exe: []const u8, +default_step: *Step, +env_map: *EnvMap, +top_level_steps: ArrayList(*TopLevelStep), +install_prefix: []const u8, +dest_dir: ?[]const u8, +lib_dir: []const u8, +exe_dir: []const u8, +h_dir: []const u8, +install_path: []const u8, +sysroot: ?[]const u8 = null, +search_prefixes: ArrayList([]const u8), +libc_file: ?[]const u8 = null, +installed_files: ArrayList(InstalledFile), +/// Path to the directory containing build.zig. +build_root: []const u8, +cache_root: []const u8, +global_cache_root: []const u8, +/// zig lib dir +override_lib_dir: ?[]const u8, +vcpkg_root: VcpkgRoot = .unattempted, +pkg_config_pkg_list: ?(PkgConfigError![]const PkgConfigPkg) = null, +args: ?[][]const u8 = null, +debug_log_scopes: []const []const u8 = &.{}, +debug_compile_errors: bool = false, + +/// Experimental. Use system Darling installation to run cross compiled macOS build artifacts. +enable_darling: bool = false, +/// Use system QEMU installation to run cross compiled foreign architecture build artifacts. +enable_qemu: bool = false, +/// Darwin. Use Rosetta to run x86_64 macOS build artifacts on arm64 macOS. +enable_rosetta: bool = false, +/// Use system Wasmtime installation to run cross compiled wasm/wasi build artifacts. +enable_wasmtime: bool = false, +/// Use system Wine installation to run cross compiled Windows build artifacts. +enable_wine: bool = false, +/// After following the steps in https://github.com/ziglang/zig/wiki/Updating-libc#glibc, +/// this will be the directory $glibc-build-dir/install/glibcs +/// Given the example of the aarch64 target, this is the directory +/// that contains the path `aarch64-linux-gnu/lib/ld-linux-aarch64.so.1`. +glibc_runtimes_dir: ?[]const u8 = null, + +/// Information about the native target. Computed before build() is invoked. +host: NativeTargetInfo, + +dep_prefix: []const u8 = "", + +pub const ExecError = error{ + ReadFailure, + ExitCodeFailure, + ProcessTerminated, + ExecNotSupported, +} || std.ChildProcess.SpawnError; + +pub const PkgConfigError = error{ + PkgConfigCrashed, + PkgConfigFailed, + PkgConfigNotInstalled, + PkgConfigInvalidOutput, +}; + +pub const PkgConfigPkg = struct { + name: []const u8, + desc: []const u8, +}; + +pub const CStd = enum { + C89, + C99, + C11, +}; + +const UserInputOptionsMap = StringHashMap(UserInputOption); +const AvailableOptionsMap = StringHashMap(AvailableOption); + +const AvailableOption = struct { + name: []const u8, + type_id: TypeId, + description: []const u8, + /// If the `type_id` is `enum` this provides the list of enum options + enum_options: ?[]const []const u8, +}; + +const UserInputOption = struct { + name: []const u8, + value: UserValue, + used: bool, +}; + +const UserValue = union(enum) { + flag: void, + scalar: []const u8, + list: ArrayList([]const u8), + map: StringHashMap(*const UserValue), +}; + +const TypeId = enum { + bool, + int, + float, + @"enum", + string, + list, +}; + +const TopLevelStep = struct { + pub const base_id = .top_level; + + step: Step, + description: []const u8, +}; + +pub const DirList = struct { + lib_dir: ?[]const u8 = null, + exe_dir: ?[]const u8 = null, + include_dir: ?[]const u8 = null, +}; + +pub fn create( + allocator: Allocator, + zig_exe: []const u8, + build_root: []const u8, + cache_root: []const u8, + global_cache_root: []const u8, + host: NativeTargetInfo, +) !*Build { + const env_map = try allocator.create(EnvMap); + env_map.* = try process.getEnvMap(allocator); + + const self = try allocator.create(Build); + self.* = Build{ + .zig_exe = zig_exe, + .build_root = build_root, + .cache_root = try fs.path.relative(allocator, build_root, cache_root), + .global_cache_root = global_cache_root, + .verbose = false, + .verbose_link = false, + .verbose_cc = false, + .verbose_air = false, + .verbose_llvm_ir = false, + .verbose_cimport = false, + .verbose_llvm_cpu_features = false, + .prominent_compile_errors = false, + .invalid_user_input = false, + .allocator = allocator, + .user_input_options = UserInputOptionsMap.init(allocator), + .available_options_map = AvailableOptionsMap.init(allocator), + .available_options_list = ArrayList(AvailableOption).init(allocator), + .top_level_steps = ArrayList(*TopLevelStep).init(allocator), + .default_step = undefined, + .env_map = env_map, + .search_prefixes = ArrayList([]const u8).init(allocator), + .install_prefix = undefined, + .lib_dir = undefined, + .exe_dir = undefined, + .h_dir = undefined, + .dest_dir = env_map.get("DESTDIR"), + .installed_files = ArrayList(InstalledFile).init(allocator), + .install_tls = TopLevelStep{ + .step = Step.initNoOp(.top_level, "install", allocator), + .description = "Copy build artifacts to prefix path", + }, + .uninstall_tls = TopLevelStep{ + .step = Step.init(.top_level, "uninstall", allocator, makeUninstall), + .description = "Remove build artifacts from prefix path", + }, + .override_lib_dir = null, + .install_path = undefined, + .args = null, + .host = host, + }; + try self.top_level_steps.append(&self.install_tls); + try self.top_level_steps.append(&self.uninstall_tls); + self.default_step = &self.install_tls.step; + return self; +} + +fn createChild( + parent: *Build, + dep_name: []const u8, + build_root: []const u8, + args: anytype, +) !*Build { + const child = try createChildOnly(parent, dep_name, build_root); + try applyArgs(child, args); + return child; +} + +fn createChildOnly(parent: *Build, dep_name: []const u8, build_root: []const u8) !*Build { + const allocator = parent.allocator; + const child = try allocator.create(Build); + child.* = .{ + .allocator = allocator, + .install_tls = .{ + .step = Step.initNoOp(.top_level, "install", allocator), + .description = "Copy build artifacts to prefix path", + }, + .uninstall_tls = .{ + .step = Step.init(.top_level, "uninstall", allocator, makeUninstall), + .description = "Remove build artifacts from prefix path", + }, + .user_input_options = UserInputOptionsMap.init(allocator), + .available_options_map = AvailableOptionsMap.init(allocator), + .available_options_list = ArrayList(AvailableOption).init(allocator), + .verbose = parent.verbose, + .verbose_link = parent.verbose_link, + .verbose_cc = parent.verbose_cc, + .verbose_air = parent.verbose_air, + .verbose_llvm_ir = parent.verbose_llvm_ir, + .verbose_cimport = parent.verbose_cimport, + .verbose_llvm_cpu_features = parent.verbose_llvm_cpu_features, + .prominent_compile_errors = parent.prominent_compile_errors, + .color = parent.color, + .reference_trace = parent.reference_trace, + .invalid_user_input = false, + .zig_exe = parent.zig_exe, + .default_step = undefined, + .env_map = parent.env_map, + .top_level_steps = ArrayList(*TopLevelStep).init(allocator), + .install_prefix = undefined, + .dest_dir = parent.dest_dir, + .lib_dir = parent.lib_dir, + .exe_dir = parent.exe_dir, + .h_dir = parent.h_dir, + .install_path = parent.install_path, + .sysroot = parent.sysroot, + .search_prefixes = ArrayList([]const u8).init(allocator), + .libc_file = parent.libc_file, + .installed_files = ArrayList(InstalledFile).init(allocator), + .build_root = build_root, + .cache_root = parent.cache_root, + .global_cache_root = parent.global_cache_root, + .override_lib_dir = parent.override_lib_dir, + .debug_log_scopes = parent.debug_log_scopes, + .debug_compile_errors = parent.debug_compile_errors, + .enable_darling = parent.enable_darling, + .enable_qemu = parent.enable_qemu, + .enable_rosetta = parent.enable_rosetta, + .enable_wasmtime = parent.enable_wasmtime, + .enable_wine = parent.enable_wine, + .glibc_runtimes_dir = parent.glibc_runtimes_dir, + .host = parent.host, + .dep_prefix = parent.fmt("{s}{s}.", .{ parent.dep_prefix, dep_name }), + }; + try child.top_level_steps.append(&child.install_tls); + try child.top_level_steps.append(&child.uninstall_tls); + child.default_step = &child.install_tls.step; + return child; +} + +fn applyArgs(b: *Build, args: anytype) !void { + inline for (@typeInfo(@TypeOf(args)).Struct.fields) |field| { + const v = @field(args, field.name); + const T = @TypeOf(v); + switch (T) { + CrossTarget => { + try b.user_input_options.put(field.name, .{ + .name = field.name, + .value = .{ .scalar = try v.zigTriple(b.allocator) }, + .used = false, + }); + try b.user_input_options.put("cpu", .{ + .name = "cpu", + .value = .{ .scalar = try serializeCpu(b.allocator, v.getCpu()) }, + .used = false, + }); + }, + []const u8 => { + try b.user_input_options.put(field.name, .{ + .name = field.name, + .value = .{ .scalar = v }, + .used = false, + }); + }, + else => switch (@typeInfo(T)) { + .Bool => { + try b.user_input_options.put(field.name, .{ + .name = field.name, + .value = .{ .scalar = if (v) "true" else "false" }, + .used = false, + }); + }, + .Enum => { + try b.user_input_options.put(field.name, .{ + .name = field.name, + .value = .{ .scalar = @tagName(v) }, + .used = false, + }); + }, + .Int => { + try b.user_input_options.put(field.name, .{ + .name = field.name, + .value = .{ .scalar = try std.fmt.allocPrint(b.allocator, "{d}", .{v}) }, + .used = false, + }); + }, + else => @compileError("option '" ++ field.name ++ "' has unsupported type: " ++ @typeName(T)), + }, + } + } + const Hasher = std.crypto.auth.siphash.SipHash128(1, 3); + // Random bytes to make unique. Refresh this with new random bytes when + // implementation is modified in a non-backwards-compatible way. + var hash = Hasher.init("ZaEsvQ5ClaA2IdH9"); + hash.update(b.dep_prefix); + // TODO additionally update the hash with `args`. + + var digest: [16]u8 = undefined; + hash.final(&digest); + var hash_basename: [digest.len * 2]u8 = undefined; + _ = std.fmt.bufPrint(&hash_basename, "{s}", .{std.fmt.fmtSliceHexLower(&digest)}) catch + unreachable; + + const install_prefix = b.pathJoin(&.{ b.cache_root, "i", &hash_basename }); + b.resolveInstallPrefix(install_prefix, .{}); +} + +pub fn destroy(self: *Build) void { + self.env_map.deinit(); + self.top_level_steps.deinit(); + self.allocator.destroy(self); +} + +/// This function is intended to be called by lib/build_runner.zig, not a build.zig file. +pub fn resolveInstallPrefix(self: *Build, install_prefix: ?[]const u8, dir_list: DirList) void { + if (self.dest_dir) |dest_dir| { + self.install_prefix = install_prefix orelse "/usr"; + self.install_path = self.pathJoin(&.{ dest_dir, self.install_prefix }); + } else { + self.install_prefix = install_prefix orelse + (self.pathJoin(&.{ self.build_root, "zig-out" })); + self.install_path = self.install_prefix; + } + + var lib_list = [_][]const u8{ self.install_path, "lib" }; + var exe_list = [_][]const u8{ self.install_path, "bin" }; + var h_list = [_][]const u8{ self.install_path, "include" }; + + if (dir_list.lib_dir) |dir| { + if (std.fs.path.isAbsolute(dir)) lib_list[0] = self.dest_dir orelse ""; + lib_list[1] = dir; + } + + if (dir_list.exe_dir) |dir| { + if (std.fs.path.isAbsolute(dir)) exe_list[0] = self.dest_dir orelse ""; + exe_list[1] = dir; + } + + if (dir_list.include_dir) |dir| { + if (std.fs.path.isAbsolute(dir)) h_list[0] = self.dest_dir orelse ""; + h_list[1] = dir; + } + + self.lib_dir = self.pathJoin(&lib_list); + self.exe_dir = self.pathJoin(&exe_list); + self.h_dir = self.pathJoin(&h_list); +} + +pub fn addOptions(self: *Build) *OptionsStep { + return OptionsStep.create(self); +} + +pub const ExecutableOptions = struct { + name: []const u8, + root_source_file: ?FileSource = null, + version: ?std.builtin.Version = null, + target: CrossTarget = .{}, + optimize: std.builtin.Mode = .Debug, + linkage: ?CompileStep.Linkage = null, +}; + +pub fn addExecutable(b: *Build, options: ExecutableOptions) *CompileStep { + return CompileStep.create(b, .{ + .name = options.name, + .root_source_file = options.root_source_file, + .version = options.version, + .target = options.target, + .optimize = options.optimize, + .kind = .exe, + .linkage = options.linkage, + }); +} + +pub const ObjectOptions = struct { + name: []const u8, + root_source_file: ?FileSource = null, + target: CrossTarget, + optimize: std.builtin.Mode, +}; + +pub fn addObject(b: *Build, options: ObjectOptions) *CompileStep { + return CompileStep.create(b, .{ + .name = options.name, + .root_source_file = options.root_source_file, + .target = options.target, + .optimize = options.optimize, + .kind = .obj, + }); +} + +pub const SharedLibraryOptions = struct { + name: []const u8, + root_source_file: ?FileSource = null, + version: ?std.builtin.Version = null, + target: CrossTarget, + optimize: std.builtin.Mode, +}; + +pub fn addSharedLibrary(b: *Build, options: SharedLibraryOptions) *CompileStep { + return CompileStep.create(b, .{ + .name = options.name, + .root_source_file = options.root_source_file, + .kind = .lib, + .linkage = .dynamic, + .version = options.version, + .target = options.target, + .optimize = options.optimize, + }); +} + +pub const StaticLibraryOptions = struct { + name: []const u8, + root_source_file: ?FileSource = null, + target: CrossTarget, + optimize: std.builtin.Mode, + version: ?std.builtin.Version = null, +}; + +pub fn addStaticLibrary(b: *Build, options: StaticLibraryOptions) *CompileStep { + return CompileStep.create(b, .{ + .name = options.name, + .root_source_file = options.root_source_file, + .kind = .lib, + .linkage = .static, + .version = options.version, + .target = options.target, + .optimize = options.optimize, + }); +} + +pub const TestOptions = struct { + name: []const u8 = "test", + kind: CompileStep.Kind = .@"test", + root_source_file: FileSource, + target: CrossTarget = .{}, + optimize: std.builtin.Mode = .Debug, + version: ?std.builtin.Version = null, +}; + +pub fn addTest(b: *Build, options: TestOptions) *CompileStep { + return CompileStep.create(b, .{ + .name = options.name, + .kind = options.kind, + .root_source_file = options.root_source_file, + .target = options.target, + .optimize = options.optimize, + }); +} + +pub const AssemblyOptions = struct { + name: []const u8, + source_file: FileSource, + target: CrossTarget, + optimize: std.builtin.Mode, +}; + +pub fn addAssembly(b: *Build, options: AssemblyOptions) *CompileStep { + const obj_step = CompileStep.create(b, .{ + .name = options.name, + .root_source_file = null, + .target = options.target, + .optimize = options.optimize, + }); + obj_step.addAssemblyFileSource(options.source_file.dupe(b)); + return obj_step; +} + +/// Initializes a RunStep with argv, which must at least have the path to the +/// executable. More command line arguments can be added with `addArg`, +/// `addArgs`, and `addArtifactArg`. +/// Be careful using this function, as it introduces a system dependency. +/// To run an executable built with zig build, see `CompileStep.run`. +pub fn addSystemCommand(self: *Build, argv: []const []const u8) *RunStep { + assert(argv.len >= 1); + const run_step = RunStep.create(self, self.fmt("run {s}", .{argv[0]})); + run_step.addArgs(argv); + return run_step; +} + +pub fn addConfigHeader( + b: *Build, + source: FileSource, + style: ConfigHeaderStep.Style, + values: anytype, +) *ConfigHeaderStep { + const config_header_step = ConfigHeaderStep.create(b, source, style); + config_header_step.addValues(values); + return config_header_step; +} + +/// Allocator.dupe without the need to handle out of memory. +pub fn dupe(self: *Build, bytes: []const u8) []u8 { + return self.allocator.dupe(u8, bytes) catch @panic("OOM"); +} + +/// Duplicates an array of strings without the need to handle out of memory. +pub fn dupeStrings(self: *Build, strings: []const []const u8) [][]u8 { + const array = self.allocator.alloc([]u8, strings.len) catch @panic("OOM"); + for (strings) |s, i| { + array[i] = self.dupe(s); + } + return array; +} + +/// Duplicates a path and converts all slashes to the OS's canonical path separator. +pub fn dupePath(self: *Build, bytes: []const u8) []u8 { + const the_copy = self.dupe(bytes); + for (the_copy) |*byte| { + switch (byte.*) { + '/', '\\' => byte.* = fs.path.sep, + else => {}, + } + } + return the_copy; +} + +/// Duplicates a package recursively. +pub fn dupePkg(self: *Build, package: Pkg) Pkg { + var the_copy = Pkg{ + .name = self.dupe(package.name), + .source = package.source.dupe(self), + }; + + if (package.dependencies) |dependencies| { + const new_dependencies = self.allocator.alloc(Pkg, dependencies.len) catch @panic("OOM"); + the_copy.dependencies = new_dependencies; + + for (dependencies) |dep_package, i| { + new_dependencies[i] = self.dupePkg(dep_package); + } + } + return the_copy; +} + +pub fn addWriteFile(self: *Build, file_path: []const u8, data: []const u8) *WriteFileStep { + const write_file_step = self.addWriteFiles(); + write_file_step.add(file_path, data); + return write_file_step; +} + +pub fn addWriteFiles(self: *Build) *WriteFileStep { + const write_file_step = self.allocator.create(WriteFileStep) catch @panic("OOM"); + write_file_step.* = WriteFileStep.init(self); + return write_file_step; +} + +pub fn addLog(self: *Build, comptime format: []const u8, args: anytype) *LogStep { + const data = self.fmt(format, args); + const log_step = self.allocator.create(LogStep) catch @panic("OOM"); + log_step.* = LogStep.init(self, data); + return log_step; +} + +pub fn addRemoveDirTree(self: *Build, dir_path: []const u8) *RemoveDirStep { + const remove_dir_step = self.allocator.create(RemoveDirStep) catch @panic("OOM"); + remove_dir_step.* = RemoveDirStep.init(self, dir_path); + return remove_dir_step; +} + +pub fn addFmt(self: *Build, paths: []const []const u8) *FmtStep { + return FmtStep.create(self, paths); +} + +pub fn addTranslateC(self: *Build, options: TranslateCStep.Options) *TranslateCStep { + return TranslateCStep.create(self, options); +} + +pub fn make(self: *Build, step_names: []const []const u8) !void { + try self.makePath(self.cache_root); + + var wanted_steps = ArrayList(*Step).init(self.allocator); + defer wanted_steps.deinit(); + + if (step_names.len == 0) { + try wanted_steps.append(self.default_step); + } else { + for (step_names) |step_name| { + const s = try self.getTopLevelStepByName(step_name); + try wanted_steps.append(s); + } + } + + for (wanted_steps.items) |s| { + try self.makeOneStep(s); + } +} + +pub fn getInstallStep(self: *Build) *Step { + return &self.install_tls.step; +} + +pub fn getUninstallStep(self: *Build) *Step { + return &self.uninstall_tls.step; +} + +fn makeUninstall(uninstall_step: *Step) anyerror!void { + const uninstall_tls = @fieldParentPtr(TopLevelStep, "step", uninstall_step); + const self = @fieldParentPtr(Build, "uninstall_tls", uninstall_tls); + + for (self.installed_files.items) |installed_file| { + const full_path = self.getInstallPath(installed_file.dir, installed_file.path); + if (self.verbose) { + log.info("rm {s}", .{full_path}); + } + fs.cwd().deleteTree(full_path) catch {}; + } + + // TODO remove empty directories +} + +fn makeOneStep(self: *Build, s: *Step) anyerror!void { + if (s.loop_flag) { + log.err("Dependency loop detected:\n {s}", .{s.name}); + return error.DependencyLoopDetected; + } + s.loop_flag = true; + + for (s.dependencies.items) |dep| { + self.makeOneStep(dep) catch |err| { + if (err == error.DependencyLoopDetected) { + log.err(" {s}", .{s.name}); + } + return err; + }; + } + + s.loop_flag = false; + + try s.make(); +} + +fn getTopLevelStepByName(self: *Build, name: []const u8) !*Step { + for (self.top_level_steps.items) |top_level_step| { + if (mem.eql(u8, top_level_step.step.name, name)) { + return &top_level_step.step; + } + } + log.err("Cannot run step '{s}' because it does not exist", .{name}); + return error.InvalidStepName; +} + +pub fn option(self: *Build, comptime T: type, name_raw: []const u8, description_raw: []const u8) ?T { + const name = self.dupe(name_raw); + const description = self.dupe(description_raw); + const type_id = comptime typeToEnum(T); + const enum_options = if (type_id == .@"enum") blk: { + const fields = comptime std.meta.fields(T); + var options = ArrayList([]const u8).initCapacity(self.allocator, fields.len) catch @panic("OOM"); + + inline for (fields) |field| { + options.appendAssumeCapacity(field.name); + } + + break :blk options.toOwnedSlice() catch @panic("OOM"); + } else null; + const available_option = AvailableOption{ + .name = name, + .type_id = type_id, + .description = description, + .enum_options = enum_options, + }; + if ((self.available_options_map.fetchPut(name, available_option) catch @panic("OOM")) != null) { + panic("Option '{s}' declared twice", .{name}); + } + self.available_options_list.append(available_option) catch @panic("OOM"); + + const option_ptr = self.user_input_options.getPtr(name) orelse return null; + option_ptr.used = true; + switch (type_id) { + .bool => switch (option_ptr.value) { + .flag => return true, + .scalar => |s| { + if (mem.eql(u8, s, "true")) { + return true; + } else if (mem.eql(u8, s, "false")) { + return false; + } else { + log.err("Expected -D{s} to be a boolean, but received '{s}'\n", .{ name, s }); + self.markInvalidUserInput(); + return null; + } + }, + .list, .map => { + log.err("Expected -D{s} to be a boolean, but received a {s}.\n", .{ + name, @tagName(option_ptr.value), + }); + self.markInvalidUserInput(); + return null; + }, + }, + .int => switch (option_ptr.value) { + .flag, .list, .map => { + log.err("Expected -D{s} to be an integer, but received a {s}.\n", .{ + name, @tagName(option_ptr.value), + }); + self.markInvalidUserInput(); + return null; + }, + .scalar => |s| { + const n = std.fmt.parseInt(T, s, 10) catch |err| switch (err) { + error.Overflow => { + log.err("-D{s} value {s} cannot fit into type {s}.\n", .{ name, s, @typeName(T) }); + self.markInvalidUserInput(); + return null; + }, + else => { + log.err("Expected -D{s} to be an integer of type {s}.\n", .{ name, @typeName(T) }); + self.markInvalidUserInput(); + return null; + }, + }; + return n; + }, + }, + .float => switch (option_ptr.value) { + .flag, .map, .list => { + log.err("Expected -D{s} to be a float, but received a {s}.\n", .{ + name, @tagName(option_ptr.value), + }); + self.markInvalidUserInput(); + return null; + }, + .scalar => |s| { + const n = std.fmt.parseFloat(T, s) catch { + log.err("Expected -D{s} to be a float of type {s}.\n", .{ name, @typeName(T) }); + self.markInvalidUserInput(); + return null; + }; + return n; + }, + }, + .@"enum" => switch (option_ptr.value) { + .flag, .map, .list => { + log.err("Expected -D{s} to be an enum, but received a {s}.\n", .{ + name, @tagName(option_ptr.value), + }); + self.markInvalidUserInput(); + return null; + }, + .scalar => |s| { + if (std.meta.stringToEnum(T, s)) |enum_lit| { + return enum_lit; + } else { + log.err("Expected -D{s} to be of type {s}.\n", .{ name, @typeName(T) }); + self.markInvalidUserInput(); + return null; + } + }, + }, + .string => switch (option_ptr.value) { + .flag, .list, .map => { + log.err("Expected -D{s} to be a string, but received a {s}.\n", .{ + name, @tagName(option_ptr.value), + }); + self.markInvalidUserInput(); + return null; + }, + .scalar => |s| return s, + }, + .list => switch (option_ptr.value) { + .flag, .map => { + log.err("Expected -D{s} to be a list, but received a {s}.\n", .{ + name, @tagName(option_ptr.value), + }); + self.markInvalidUserInput(); + return null; + }, + .scalar => |s| { + return self.allocator.dupe([]const u8, &[_][]const u8{s}) catch @panic("OOM"); + }, + .list => |lst| return lst.items, + }, + } +} + +pub fn step(self: *Build, name: []const u8, description: []const u8) *Step { + const step_info = self.allocator.create(TopLevelStep) catch @panic("OOM"); + step_info.* = TopLevelStep{ + .step = Step.initNoOp(.top_level, name, self.allocator), + .description = self.dupe(description), + }; + self.top_level_steps.append(step_info) catch @panic("OOM"); + return &step_info.step; +} + +pub const StandardOptimizeOptionOptions = struct { + preferred_optimize_mode: ?std.builtin.Mode = null, +}; + +pub fn standardOptimizeOption(self: *Build, options: StandardOptimizeOptionOptions) std.builtin.Mode { + if (options.preferred_optimize_mode) |mode| { + if (self.option(bool, "release", "optimize for end users") orelse false) { + return mode; + } else { + return .Debug; + } + } else { + return self.option( + std.builtin.Mode, + "optimize", + "prioritize performance, safety, or binary size (-O flag)", + ) orelse .Debug; + } +} + +pub const StandardTargetOptionsArgs = struct { + whitelist: ?[]const CrossTarget = null, + + default_target: CrossTarget = CrossTarget{}, +}; + +/// Exposes standard `zig build` options for choosing a target. +pub fn standardTargetOptions(self: *Build, args: StandardTargetOptionsArgs) CrossTarget { + const maybe_triple = self.option( + []const u8, + "target", + "The CPU architecture, OS, and ABI to build for", + ); + const mcpu = self.option([]const u8, "cpu", "Target CPU features to add or subtract"); + + if (maybe_triple == null and mcpu == null) { + return args.default_target; + } + + const triple = maybe_triple orelse "native"; + + var diags: CrossTarget.ParseOptions.Diagnostics = .{}; + const selected_target = CrossTarget.parse(.{ + .arch_os_abi = triple, + .cpu_features = mcpu, + .diagnostics = &diags, + }) catch |err| switch (err) { + error.UnknownCpuModel => { + log.err("Unknown CPU: '{s}'\nAvailable CPUs for architecture '{s}':", .{ + diags.cpu_name.?, + @tagName(diags.arch.?), + }); + for (diags.arch.?.allCpuModels()) |cpu| { + log.err(" {s}", .{cpu.name}); + } + self.markInvalidUserInput(); + return args.default_target; + }, + error.UnknownCpuFeature => { + log.err( + \\Unknown CPU feature: '{s}' + \\Available CPU features for architecture '{s}': + \\ + , .{ + diags.unknown_feature_name.?, + @tagName(diags.arch.?), + }); + for (diags.arch.?.allFeaturesList()) |feature| { + log.err(" {s}: {s}", .{ feature.name, feature.description }); + } + self.markInvalidUserInput(); + return args.default_target; + }, + error.UnknownOperatingSystem => { + log.err( + \\Unknown OS: '{s}' + \\Available operating systems: + \\ + , .{diags.os_name.?}); + inline for (std.meta.fields(std.Target.Os.Tag)) |field| { + log.err(" {s}", .{field.name}); + } + self.markInvalidUserInput(); + return args.default_target; + }, + else => |e| { + log.err("Unable to parse target '{s}': {s}\n", .{ triple, @errorName(e) }); + self.markInvalidUserInput(); + return args.default_target; + }, + }; + + const selected_canonicalized_triple = selected_target.zigTriple(self.allocator) catch @panic("OOM"); + + if (args.whitelist) |list| whitelist_check: { + // Make sure it's a match of one of the list. + var mismatch_triple = true; + var mismatch_cpu_features = true; + var whitelist_item = CrossTarget{}; + for (list) |t| { + mismatch_cpu_features = true; + mismatch_triple = true; + + const t_triple = t.zigTriple(self.allocator) catch @panic("OOM"); + if (mem.eql(u8, t_triple, selected_canonicalized_triple)) { + mismatch_triple = false; + whitelist_item = t; + if (t.getCpuFeatures().isSuperSetOf(selected_target.getCpuFeatures())) { + mismatch_cpu_features = false; + break :whitelist_check; + } else { + break; + } + } + } + if (mismatch_triple) { + log.err("Chosen target '{s}' does not match one of the supported targets:", .{ + selected_canonicalized_triple, + }); + for (list) |t| { + const t_triple = t.zigTriple(self.allocator) catch @panic("OOM"); + log.err(" {s}", .{t_triple}); + } + } else { + assert(mismatch_cpu_features); + const whitelist_cpu = whitelist_item.getCpu(); + const selected_cpu = selected_target.getCpu(); + log.err("Chosen CPU model '{s}' does not match one of the supported targets:", .{ + selected_cpu.model.name, + }); + log.err(" Supported feature Set: ", .{}); + const all_features = whitelist_cpu.arch.allFeaturesList(); + var populated_cpu_features = whitelist_cpu.model.features; + populated_cpu_features.populateDependencies(all_features); + for (all_features) |feature, i_usize| { + const i = @intCast(std.Target.Cpu.Feature.Set.Index, i_usize); + const in_cpu_set = populated_cpu_features.isEnabled(i); + if (in_cpu_set) { + log.err("{s} ", .{feature.name}); + } + } + log.err(" Remove: ", .{}); + for (all_features) |feature, i_usize| { + const i = @intCast(std.Target.Cpu.Feature.Set.Index, i_usize); + const in_cpu_set = populated_cpu_features.isEnabled(i); + const in_actual_set = selected_cpu.features.isEnabled(i); + if (in_actual_set and !in_cpu_set) { + log.err("{s} ", .{feature.name}); + } + } + } + self.markInvalidUserInput(); + return args.default_target; + } + + return selected_target; +} + +pub fn addUserInputOption(self: *Build, name_raw: []const u8, value_raw: []const u8) !bool { + const name = self.dupe(name_raw); + const value = self.dupe(value_raw); + const gop = try self.user_input_options.getOrPut(name); + if (!gop.found_existing) { + gop.value_ptr.* = UserInputOption{ + .name = name, + .value = .{ .scalar = value }, + .used = false, + }; + return false; + } + + // option already exists + switch (gop.value_ptr.value) { + .scalar => |s| { + // turn it into a list + var list = ArrayList([]const u8).init(self.allocator); + try list.append(s); + try list.append(value); + try self.user_input_options.put(name, .{ + .name = name, + .value = .{ .list = list }, + .used = false, + }); + }, + .list => |*list| { + // append to the list + try list.append(value); + try self.user_input_options.put(name, .{ + .name = name, + .value = .{ .list = list.* }, + .used = false, + }); + }, + .flag => { + log.warn("Option '-D{s}={s}' conflicts with flag '-D{s}'.", .{ name, value, name }); + return true; + }, + .map => |*map| { + _ = map; + log.warn("TODO maps as command line arguments is not implemented yet.", .{}); + return true; + }, + } + return false; +} + +pub fn addUserInputFlag(self: *Build, name_raw: []const u8) !bool { + const name = self.dupe(name_raw); + const gop = try self.user_input_options.getOrPut(name); + if (!gop.found_existing) { + gop.value_ptr.* = .{ + .name = name, + .value = .{ .flag = {} }, + .used = false, + }; + return false; + } + + // option already exists + switch (gop.value_ptr.value) { + .scalar => |s| { + log.err("Flag '-D{s}' conflicts with option '-D{s}={s}'.", .{ name, name, s }); + return true; + }, + .list, .map => { + log.err("Flag '-D{s}' conflicts with multiple options of the same name.", .{name}); + return true; + }, + .flag => {}, + } + return false; +} + +fn typeToEnum(comptime T: type) TypeId { + return switch (@typeInfo(T)) { + .Int => .int, + .Float => .float, + .Bool => .bool, + .Enum => .@"enum", + else => switch (T) { + []const u8 => .string, + []const []const u8 => .list, + else => @compileError("Unsupported type: " ++ @typeName(T)), + }, + }; +} + +fn markInvalidUserInput(self: *Build) void { + self.invalid_user_input = true; +} + +pub fn validateUserInputDidItFail(self: *Build) bool { + // make sure all args are used + var it = self.user_input_options.iterator(); + while (it.next()) |entry| { + if (!entry.value_ptr.used) { + log.err("Invalid option: -D{s}", .{entry.key_ptr.*}); + self.markInvalidUserInput(); + } + } + + return self.invalid_user_input; +} + +pub fn spawnChild(self: *Build, argv: []const []const u8) !void { + return self.spawnChildEnvMap(null, self.env_map, argv); +} + +fn printCmd(cwd: ?[]const u8, argv: []const []const u8) void { + if (cwd) |yes_cwd| std.debug.print("cd {s} && ", .{yes_cwd}); + for (argv) |arg| { + std.debug.print("{s} ", .{arg}); + } + std.debug.print("\n", .{}); +} + +pub fn spawnChildEnvMap(self: *Build, cwd: ?[]const u8, env_map: *const EnvMap, argv: []const []const u8) !void { + if (self.verbose) { + printCmd(cwd, argv); + } + + if (!std.process.can_spawn) + return error.ExecNotSupported; + + var child = std.ChildProcess.init(argv, self.allocator); + child.cwd = cwd; + child.env_map = env_map; + + const term = child.spawnAndWait() catch |err| { + log.err("Unable to spawn {s}: {s}", .{ argv[0], @errorName(err) }); + return err; + }; + + switch (term) { + .Exited => |code| { + if (code != 0) { + log.err("The following command exited with error code {}:", .{code}); + printCmd(cwd, argv); + return error.UncleanExit; + } + }, + else => { + log.err("The following command terminated unexpectedly:", .{}); + printCmd(cwd, argv); + + return error.UncleanExit; + }, + } +} + +pub fn makePath(self: *Build, path: []const u8) !void { + fs.cwd().makePath(self.pathFromRoot(path)) catch |err| { + log.err("Unable to create path {s}: {s}", .{ path, @errorName(err) }); + return err; + }; +} + +pub fn installArtifact(self: *Build, artifact: *CompileStep) void { + self.getInstallStep().dependOn(&self.addInstallArtifact(artifact).step); +} + +pub fn addInstallArtifact(self: *Build, artifact: *CompileStep) *InstallArtifactStep { + return InstallArtifactStep.create(self, artifact); +} + +///`dest_rel_path` is relative to prefix path +pub fn installFile(self: *Build, src_path: []const u8, dest_rel_path: []const u8) void { + self.getInstallStep().dependOn(&self.addInstallFileWithDir(.{ .path = src_path }, .prefix, dest_rel_path).step); +} + +pub fn installDirectory(self: *Build, options: InstallDirectoryOptions) void { + self.getInstallStep().dependOn(&self.addInstallDirectory(options).step); +} + +///`dest_rel_path` is relative to bin path +pub fn installBinFile(self: *Build, src_path: []const u8, dest_rel_path: []const u8) void { + self.getInstallStep().dependOn(&self.addInstallFileWithDir(.{ .path = src_path }, .bin, dest_rel_path).step); +} + +///`dest_rel_path` is relative to lib path +pub fn installLibFile(self: *Build, src_path: []const u8, dest_rel_path: []const u8) void { + self.getInstallStep().dependOn(&self.addInstallFileWithDir(.{ .path = src_path }, .lib, dest_rel_path).step); +} + +/// Output format (BIN vs Intel HEX) determined by filename +pub fn installRaw(self: *Build, artifact: *CompileStep, dest_filename: []const u8, options: InstallRawStep.CreateOptions) *InstallRawStep { + const raw = self.addInstallRaw(artifact, dest_filename, options); + self.getInstallStep().dependOn(&raw.step); + return raw; +} + +///`dest_rel_path` is relative to install prefix path +pub fn addInstallFile(self: *Build, source: FileSource, dest_rel_path: []const u8) *InstallFileStep { + return self.addInstallFileWithDir(source.dupe(self), .prefix, dest_rel_path); +} + +///`dest_rel_path` is relative to bin path +pub fn addInstallBinFile(self: *Build, source: FileSource, dest_rel_path: []const u8) *InstallFileStep { + return self.addInstallFileWithDir(source.dupe(self), .bin, dest_rel_path); +} + +///`dest_rel_path` is relative to lib path +pub fn addInstallLibFile(self: *Build, source: FileSource, dest_rel_path: []const u8) *InstallFileStep { + return self.addInstallFileWithDir(source.dupe(self), .lib, dest_rel_path); +} + +pub fn addInstallHeaderFile(b: *Build, src_path: []const u8, dest_rel_path: []const u8) *InstallFileStep { + return b.addInstallFileWithDir(.{ .path = src_path }, .header, dest_rel_path); +} + +pub fn addInstallRaw(self: *Build, artifact: *CompileStep, dest_filename: []const u8, options: InstallRawStep.CreateOptions) *InstallRawStep { + return InstallRawStep.create(self, artifact, dest_filename, options); +} + +pub fn addInstallFileWithDir( + self: *Build, + source: FileSource, + install_dir: InstallDir, + dest_rel_path: []const u8, +) *InstallFileStep { + if (dest_rel_path.len == 0) { + panic("dest_rel_path must be non-empty", .{}); + } + const install_step = self.allocator.create(InstallFileStep) catch @panic("OOM"); + install_step.* = InstallFileStep.init(self, source.dupe(self), install_dir, dest_rel_path); + return install_step; +} + +pub fn addInstallDirectory(self: *Build, options: InstallDirectoryOptions) *InstallDirStep { + const install_step = self.allocator.create(InstallDirStep) catch @panic("OOM"); + install_step.* = InstallDirStep.init(self, options); + return install_step; +} + +pub fn pushInstalledFile(self: *Build, dir: InstallDir, dest_rel_path: []const u8) void { + const file = InstalledFile{ + .dir = dir, + .path = dest_rel_path, + }; + self.installed_files.append(file.dupe(self)) catch @panic("OOM"); +} + +pub fn updateFile(self: *Build, source_path: []const u8, dest_path: []const u8) !void { + if (self.verbose) { + log.info("cp {s} {s} ", .{ source_path, dest_path }); + } + const cwd = fs.cwd(); + const prev_status = try fs.Dir.updateFile(cwd, source_path, cwd, dest_path, .{}); + if (self.verbose) switch (prev_status) { + .stale => log.info("# installed", .{}), + .fresh => log.info("# up-to-date", .{}), + }; +} + +pub fn truncateFile(self: *Build, dest_path: []const u8) !void { + if (self.verbose) { + log.info("truncate {s}", .{dest_path}); + } + const cwd = fs.cwd(); + var src_file = cwd.createFile(dest_path, .{}) catch |err| switch (err) { + error.FileNotFound => blk: { + if (fs.path.dirname(dest_path)) |dirname| { + try cwd.makePath(dirname); + } + break :blk try cwd.createFile(dest_path, .{}); + }, + else => |e| return e, + }; + src_file.close(); +} + +pub fn pathFromRoot(self: *Build, rel_path: []const u8) []u8 { + return fs.path.resolve(self.allocator, &[_][]const u8{ self.build_root, rel_path }) catch @panic("OOM"); +} + +pub fn pathJoin(self: *Build, paths: []const []const u8) []u8 { + return fs.path.join(self.allocator, paths) catch @panic("OOM"); +} + +pub fn fmt(self: *Build, comptime format: []const u8, args: anytype) []u8 { + return fmt_lib.allocPrint(self.allocator, format, args) catch @panic("OOM"); +} + +pub fn findProgram(self: *Build, names: []const []const u8, paths: []const []const u8) ![]const u8 { + // TODO report error for ambiguous situations + const exe_extension = @as(CrossTarget, .{}).exeFileExt(); + for (self.search_prefixes.items) |search_prefix| { + for (names) |name| { + if (fs.path.isAbsolute(name)) { + return name; + } + const full_path = self.pathJoin(&.{ + search_prefix, + "bin", + self.fmt("{s}{s}", .{ name, exe_extension }), + }); + return fs.realpathAlloc(self.allocator, full_path) catch continue; + } + } + if (self.env_map.get("PATH")) |PATH| { + for (names) |name| { + if (fs.path.isAbsolute(name)) { + return name; + } + var it = mem.tokenize(u8, PATH, &[_]u8{fs.path.delimiter}); + while (it.next()) |path| { + const full_path = self.pathJoin(&.{ + path, + self.fmt("{s}{s}", .{ name, exe_extension }), + }); + return fs.realpathAlloc(self.allocator, full_path) catch continue; + } + } + } + for (names) |name| { + if (fs.path.isAbsolute(name)) { + return name; + } + for (paths) |path| { + const full_path = self.pathJoin(&.{ + path, + self.fmt("{s}{s}", .{ name, exe_extension }), + }); + return fs.realpathAlloc(self.allocator, full_path) catch continue; + } + } + return error.FileNotFound; +} + +pub fn execAllowFail( + self: *Build, + argv: []const []const u8, + out_code: *u8, + stderr_behavior: std.ChildProcess.StdIo, +) ExecError![]u8 { + assert(argv.len != 0); + + if (!std.process.can_spawn) + return error.ExecNotSupported; + + const max_output_size = 400 * 1024; + var child = std.ChildProcess.init(argv, self.allocator); + child.stdin_behavior = .Ignore; + child.stdout_behavior = .Pipe; + child.stderr_behavior = stderr_behavior; + child.env_map = self.env_map; + + try child.spawn(); + + const stdout = child.stdout.?.reader().readAllAlloc(self.allocator, max_output_size) catch { + return error.ReadFailure; + }; + errdefer self.allocator.free(stdout); + + const term = try child.wait(); + switch (term) { + .Exited => |code| { + if (code != 0) { + out_code.* = @truncate(u8, code); + return error.ExitCodeFailure; + } + return stdout; + }, + .Signal, .Stopped, .Unknown => |code| { + out_code.* = @truncate(u8, code); + return error.ProcessTerminated; + }, + } +} + +pub fn execFromStep(self: *Build, argv: []const []const u8, src_step: ?*Step) ![]u8 { + assert(argv.len != 0); + + if (self.verbose) { + printCmd(null, argv); + } + + if (!std.process.can_spawn) { + if (src_step) |s| log.err("{s}...", .{s.name}); + log.err("Unable to spawn the following command: cannot spawn child process", .{}); + printCmd(null, argv); + std.os.abort(); + } + + var code: u8 = undefined; + return self.execAllowFail(argv, &code, .Inherit) catch |err| switch (err) { + error.ExecNotSupported => { + if (src_step) |s| log.err("{s}...", .{s.name}); + log.err("Unable to spawn the following command: cannot spawn child process", .{}); + printCmd(null, argv); + std.os.abort(); + }, + error.FileNotFound => { + if (src_step) |s| log.err("{s}...", .{s.name}); + log.err("Unable to spawn the following command: file not found", .{}); + printCmd(null, argv); + std.os.exit(@truncate(u8, code)); + }, + error.ExitCodeFailure => { + if (src_step) |s| log.err("{s}...", .{s.name}); + if (self.prominent_compile_errors) { + log.err("The step exited with error code {d}", .{code}); + } else { + log.err("The following command exited with error code {d}:", .{code}); + printCmd(null, argv); + } + + std.os.exit(@truncate(u8, code)); + }, + error.ProcessTerminated => { + if (src_step) |s| log.err("{s}...", .{s.name}); + log.err("The following command terminated unexpectedly:", .{}); + printCmd(null, argv); + std.os.exit(@truncate(u8, code)); + }, + else => |e| return e, + }; +} + +pub fn exec(self: *Build, argv: []const []const u8) ![]u8 { + return self.execFromStep(argv, null); +} + +pub fn addSearchPrefix(self: *Build, search_prefix: []const u8) void { + self.search_prefixes.append(self.dupePath(search_prefix)) catch @panic("OOM"); +} + +pub fn getInstallPath(self: *Build, dir: InstallDir, dest_rel_path: []const u8) []const u8 { + assert(!fs.path.isAbsolute(dest_rel_path)); // Install paths must be relative to the prefix + const base_dir = switch (dir) { + .prefix => self.install_path, + .bin => self.exe_dir, + .lib => self.lib_dir, + .header => self.h_dir, + .custom => |path| self.pathJoin(&.{ self.install_path, path }), + }; + return fs.path.resolve( + self.allocator, + &[_][]const u8{ base_dir, dest_rel_path }, + ) catch @panic("OOM"); +} + +pub const Dependency = struct { + builder: *Build, + + pub fn artifact(d: *Dependency, name: []const u8) *CompileStep { + var found: ?*CompileStep = null; + for (d.builder.install_tls.step.dependencies.items) |dep_step| { + const inst = dep_step.cast(InstallArtifactStep) orelse continue; + if (mem.eql(u8, inst.artifact.name, name)) { + if (found != null) panic("artifact name '{s}' is ambiguous", .{name}); + found = inst.artifact; + } + } + return found orelse { + for (d.builder.install_tls.step.dependencies.items) |dep_step| { + const inst = dep_step.cast(InstallArtifactStep) orelse continue; + log.info("available artifact: '{s}'", .{inst.artifact.name}); + } + panic("unable to find artifact '{s}'", .{name}); + }; + } +}; + +pub fn dependency(b: *Build, name: []const u8, args: anytype) *Dependency { + const build_runner = @import("root"); + const deps = build_runner.dependencies; + + inline for (@typeInfo(deps.imports).Struct.decls) |decl| { + if (mem.startsWith(u8, decl.name, b.dep_prefix) and + mem.endsWith(u8, decl.name, name) and + decl.name.len == b.dep_prefix.len + name.len) + { + const build_zig = @field(deps.imports, decl.name); + const build_root = @field(deps.build_root, decl.name); + return dependencyInner(b, name, build_root, build_zig, args); + } + } + + const full_path = b.pathFromRoot("build.zig.ini"); + std.debug.print("no dependency named '{s}' in '{s}'\n", .{ name, full_path }); + std.process.exit(1); +} + +fn dependencyInner( + b: *Build, + name: []const u8, + build_root: []const u8, + comptime build_zig: type, + args: anytype, +) *Dependency { + const sub_builder = b.createChild(name, build_root, args) catch @panic("unhandled error"); + sub_builder.runBuild(build_zig) catch @panic("unhandled error"); + + if (sub_builder.validateUserInputDidItFail()) { + std.debug.dumpCurrentStackTrace(@returnAddress()); + } + + const dep = b.allocator.create(Dependency) catch @panic("OOM"); + dep.* = .{ .builder = sub_builder }; + return dep; +} + +pub fn runBuild(b: *Build, build_zig: anytype) anyerror!void { + switch (@typeInfo(@typeInfo(@TypeOf(build_zig.build)).Fn.return_type.?)) { + .Void => build_zig.build(b), + .ErrorUnion => try build_zig.build(b), + else => @compileError("expected return type of build to be 'void' or '!void'"), + } +} + +test "builder.findProgram compiles" { + if (builtin.os.tag == .wasi) return error.SkipZigTest; + + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena.deinit(); + + const host = try NativeTargetInfo.detect(.{}); + + const builder = try Build.create( + arena.allocator(), + "zig", + "zig-cache", + "zig-cache", + "zig-cache", + host, + ); + defer builder.destroy(); + _ = builder.findProgram(&[_][]const u8{}, &[_][]const u8{}) catch null; +} + +pub const Pkg = struct { + name: []const u8, + source: FileSource, + dependencies: ?[]const Pkg = null, +}; + +/// A file that is generated by a build step. +/// This struct is an interface that is meant to be used with `@fieldParentPtr` to implement the actual path logic. +pub const GeneratedFile = struct { + /// The step that generates the file + step: *Step, + + /// The path to the generated file. Must be either absolute or relative to the build root. + /// This value must be set in the `fn make()` of the `step` and must not be `null` afterwards. + path: ?[]const u8 = null, + + pub fn getPath(self: GeneratedFile) []const u8 { + return self.path orelse std.debug.panic( + "getPath() was called on a GeneratedFile that wasn't build yet. Is there a missing Step dependency on step '{s}'?", + .{self.step.name}, + ); + } +}; + +/// A file source is a reference to an existing or future file. +/// +pub const FileSource = union(enum) { + /// A plain file path, relative to build root or absolute. + path: []const u8, + + /// A file that is generated by an interface. Those files usually are + /// not available until built by a build step. + generated: *const GeneratedFile, + + /// Returns a new file source that will have a relative path to the build root guaranteed. + /// This should be preferred over setting `.path` directly as it documents that the files are in the project directory. + pub fn relative(path: []const u8) FileSource { + std.debug.assert(!std.fs.path.isAbsolute(path)); + return FileSource{ .path = path }; + } + + /// Returns a string that can be shown to represent the file source. + /// Either returns the path or `"generated"`. + pub fn getDisplayName(self: FileSource) []const u8 { + return switch (self) { + .path => self.path, + .generated => "generated", + }; + } + + /// Adds dependencies this file source implies to the given step. + pub fn addStepDependencies(self: FileSource, other_step: *Step) void { + switch (self) { + .path => {}, + .generated => |gen| other_step.dependOn(gen.step), + } + } + + /// Should only be called during make(), returns a path relative to the build root or absolute. + pub fn getPath(self: FileSource, builder: *Build) []const u8 { + const path = switch (self) { + .path => |p| builder.pathFromRoot(p), + .generated => |gen| gen.getPath(), + }; + return path; + } + + /// Duplicates the file source for a given builder. + pub fn dupe(self: FileSource, b: *Build) FileSource { + return switch (self) { + .path => |p| .{ .path = b.dupePath(p) }, + .generated => |gen| .{ .generated = gen }, + }; + } +}; + +/// Allocates a new string for assigning a value to a named macro. +/// If the value is omitted, it is set to 1. +/// `name` and `value` need not live longer than the function call. +pub fn constructCMacro(allocator: Allocator, name: []const u8, value: ?[]const u8) []const u8 { + var macro = allocator.alloc( + u8, + name.len + if (value) |value_slice| value_slice.len + 1 else 0, + ) catch |err| if (err == error.OutOfMemory) @panic("Out of memory") else unreachable; + mem.copy(u8, macro, name); + if (value) |value_slice| { + macro[name.len] = '='; + mem.copy(u8, macro[name.len + 1 ..], value_slice); + } + return macro; +} + +pub const VcpkgRoot = union(VcpkgRootStatus) { + unattempted: void, + not_found: void, + found: []const u8, +}; + +pub const VcpkgRootStatus = enum { + unattempted, + not_found, + found, +}; + +pub const InstallDir = union(enum) { + prefix: void, + lib: void, + bin: void, + header: void, + /// A path relative to the prefix + custom: []const u8, + + /// Duplicates the install directory including the path if set to custom. + pub fn dupe(self: InstallDir, builder: *Build) InstallDir { + if (self == .custom) { + // Written with this temporary to avoid RLS problems + const duped_path = builder.dupe(self.custom); + return .{ .custom = duped_path }; + } else { + return self; + } + } +}; + +pub const InstalledFile = struct { + dir: InstallDir, + path: []const u8, + + /// Duplicates the installed file path and directory. + pub fn dupe(self: InstalledFile, builder: *Build) InstalledFile { + return .{ + .dir = self.dir.dupe(builder), + .path = builder.dupe(self.path), + }; + } +}; + +pub fn serializeCpu(allocator: Allocator, cpu: std.Target.Cpu) ![]const u8 { + // TODO this logic can disappear if cpu model + features becomes part of the target triple + const all_features = cpu.arch.allFeaturesList(); + var populated_cpu_features = cpu.model.features; + populated_cpu_features.populateDependencies(all_features); + + if (populated_cpu_features.eql(cpu.features)) { + // The CPU name alone is sufficient. + return cpu.model.name; + } else { + var mcpu_buffer = ArrayList(u8).init(allocator); + try mcpu_buffer.appendSlice(cpu.model.name); + + for (all_features) |feature, i_usize| { + const i = @intCast(std.Target.Cpu.Feature.Set.Index, i_usize); + const in_cpu_set = populated_cpu_features.isEnabled(i); + const in_actual_set = cpu.features.isEnabled(i); + if (in_cpu_set and !in_actual_set) { + try mcpu_buffer.writer().print("-{s}", .{feature.name}); + } else if (!in_cpu_set and in_actual_set) { + try mcpu_buffer.writer().print("+{s}", .{feature.name}); + } + } + + return try mcpu_buffer.toOwnedSlice(); + } +} + +test "dupePkg()" { + if (builtin.os.tag == .wasi) return error.SkipZigTest; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const host = try NativeTargetInfo.detect(.{}); + + var builder = try Build.create( + arena.allocator(), + "test", + "test", + "test", + "test", + host, + ); + defer builder.destroy(); + + var pkg_dep = Pkg{ + .name = "pkg_dep", + .source = .{ .path = "/not/a/pkg_dep.zig" }, + }; + var pkg_top = Pkg{ + .name = "pkg_top", + .source = .{ .path = "/not/a/pkg_top.zig" }, + .dependencies = &[_]Pkg{pkg_dep}, + }; + const duped = builder.dupePkg(pkg_top); + + const original_deps = pkg_top.dependencies.?; + const dupe_deps = duped.dependencies.?; + + // probably the same top level package details + try std.testing.expectEqualStrings(pkg_top.name, duped.name); + + // probably the same dependencies + try std.testing.expectEqual(original_deps.len, dupe_deps.len); + try std.testing.expectEqual(original_deps[0].name, pkg_dep.name); + + // could segfault otherwise if pointers in duplicated package's fields are + // the same as those in stack allocated package's fields + try std.testing.expect(dupe_deps.ptr != original_deps.ptr); + try std.testing.expect(duped.name.ptr != pkg_top.name.ptr); + try std.testing.expect(duped.source.path.ptr != pkg_top.source.path.ptr); + try std.testing.expect(dupe_deps[0].name.ptr != pkg_dep.name.ptr); + try std.testing.expect(dupe_deps[0].source.path.ptr != pkg_dep.source.path.ptr); +} + +test { + _ = CheckFileStep; + _ = CheckObjectStep; + _ = EmulatableRunStep; + _ = FmtStep; + _ = InstallArtifactStep; + _ = InstallDirStep; + _ = InstallFileStep; + _ = InstallRawStep; + _ = CompileStep; + _ = LogStep; + _ = OptionsStep; + _ = RemoveDirStep; + _ = RunStep; + _ = TranslateCStep; + _ = WriteFileStep; +} |
