Commit 4f742c4cfc

Jacob Young <jacobly0@users.noreply.github.com>
2024-07-19 07:04:59
dev: introduce dev environments that enable compiler feature sets
1 parent b7e48c6
src/codegen/llvm.zig
@@ -868,7 +868,6 @@ pub const Object = struct {
     pub const TypeMap = std.AutoHashMapUnmanaged(InternPool.Index, Builder.Type);
 
     pub fn create(arena: Allocator, comp: *Compilation) !*Object {
-        if (build_options.only_c) unreachable;
         const gpa = comp.gpa;
         const target = comp.root_mod.resolved_target.result;
         const llvm_target_triple = try targetTriple(arena, target);
src/link/Coff/lld.zig
@@ -2,6 +2,7 @@ const std = @import("std");
 const build_options = @import("build_options");
 const allocPrint = std.fmt.allocPrint;
 const assert = std.debug.assert;
+const dev = @import("../../dev.zig");
 const fs = std.fs;
 const log = std.log.scoped(.link);
 const mem = std.mem;
@@ -18,6 +19,8 @@ const Compilation = @import("../../Compilation.zig");
 const Zcu = @import("../../Zcu.zig");
 
 pub fn linkWithLLD(self: *Coff, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) !void {
+    dev.check(.lld_linker);
+
     const tracy = trace(@src());
     defer tracy.end();
 
@@ -77,10 +80,8 @@ pub fn linkWithLLD(self: *Coff, arena: Allocator, tid: Zcu.PerThread.Id, prog_no
         for (comp.c_object_table.keys()) |key| {
             _ = try man.addFile(key.status.success.object_path, null);
         }
-        if (!build_options.only_core_functionality) {
-            for (comp.win32_resource_table.keys()) |key| {
-                _ = try man.addFile(key.status.success.res_path, null);
-            }
+        for (comp.win32_resource_table.keys()) |key| {
+            _ = try man.addFile(key.status.success.res_path, null);
         }
         try man.addOptionalFile(module_obj_path);
         man.hash.addOptionalBytes(entry_name);
@@ -274,10 +275,8 @@ pub fn linkWithLLD(self: *Coff, arena: Allocator, tid: Zcu.PerThread.Id, prog_no
             try argv.append(key.status.success.object_path);
         }
 
-        if (!build_options.only_core_functionality) {
-            for (comp.win32_resource_table.keys()) |key| {
-                try argv.append(key.status.success.res_path);
-            }
+        for (comp.win32_resource_table.keys()) |key| {
+            try argv.append(key.status.success.res_path);
         }
 
         if (module_obj_path) |p| {
src/link/Elf.zig
@@ -2148,6 +2148,8 @@ fn scanRelocs(self: *Elf) !void {
 }
 
 fn linkWithLLD(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) !void {
+    dev.check(.lld_linker);
+
     const tracy = trace(@src());
     defer tracy.end();
 
@@ -6430,6 +6432,7 @@ const math = std.math;
 const mem = std.mem;
 
 const codegen = @import("../codegen.zig");
+const dev = @import("../dev.zig");
 const eh_frame = @import("Elf/eh_frame.zig");
 const gc = @import("Elf/gc.zig");
 const glibc = @import("../glibc.zig");
src/link/Wasm.zig
@@ -6,6 +6,7 @@ const assert = std.debug.assert;
 const build_options = @import("build_options");
 const builtin = @import("builtin");
 const codegen = @import("../codegen.zig");
+const dev = @import("../dev.zig");
 const fs = std.fs;
 const leb = std.leb;
 const link = @import("../link.zig");
@@ -3325,6 +3326,8 @@ fn emitImport(wasm: *Wasm, writer: anytype, import: types.Import) !void {
 }
 
 fn linkWithLLD(wasm: *Wasm, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) !void {
+    dev.check(.lld_linker);
+
     const tracy = trace(@src());
     defer tracy.end();
 
src/Zcu/PerThread.zig
@@ -64,6 +64,7 @@ pub fn astGenFile(
     path_digest: Cache.BinDigest,
     opt_root_decl: Zcu.Decl.OptionalIndex,
 ) !void {
+    dev.check(.ast_gen);
     assert(!file.mod.isBuiltin());
 
     const tracy = trace(@src());
@@ -504,6 +505,8 @@ pub fn ensureFileAnalyzed(pt: Zcu.PerThread, file_index: Zcu.File.Index) Zcu.Sem
 /// For example an inferred error set is not resolved until after `analyzeFnBody`.
 /// is called.
 pub fn ensureDeclAnalyzed(pt: Zcu.PerThread, decl_index: Zcu.Decl.Index) Zcu.SemaError!void {
+    dev.check(.sema);
+
     const tracy = trace(@src());
     defer tracy.end();
 
@@ -552,9 +555,9 @@ pub fn ensureDeclAnalyzed(pt: Zcu.PerThread, decl_index: Zcu.Decl.Index) Zcu.Sem
     }
 
     if (was_outdated) {
+        dev.check(.incremental);
         // The exports this Decl performs will be re-discovered, so we remove them here
         // prior to re-analysis.
-        if (build_options.only_c) unreachable;
         mod.deleteUnitExports(decl_as_depender);
         mod.deleteUnitReferences(decl_as_depender);
     }
@@ -623,6 +626,8 @@ pub fn ensureDeclAnalyzed(pt: Zcu.PerThread, decl_index: Zcu.Decl.Index) Zcu.Sem
 }
 
 pub fn ensureFuncBodyAnalyzed(pt: Zcu.PerThread, maybe_coerced_func_index: InternPool.Index) Zcu.SemaError!void {
+    dev.check(.sema);
+
     const tracy = trace(@src());
     defer tracy.end();
 
@@ -684,7 +689,7 @@ pub fn ensureFuncBodyAnalyzed(pt: Zcu.PerThread, maybe_coerced_func_index: Inter
         zcu.potentially_outdated.swapRemove(func_as_depender);
 
     if (was_outdated) {
-        if (build_options.only_c) unreachable;
+        dev.check(.incremental);
         _ = zcu.outdated_ready.swapRemove(func_as_depender);
         zcu.deleteUnitExports(func_as_depender);
         zcu.deleteUnitReferences(func_as_depender);
@@ -836,7 +841,6 @@ pub fn linkerUpdateFunc(pt: Zcu.PerThread, func_index: InternPool.Index, air: Ai
             },
         };
     } else if (zcu.llvm_object) |llvm_object| {
-        if (build_options.only_c) unreachable;
         llvm_object.updateFunc(pt, func_index, air, liveness) catch |err| switch (err) {
             error.OutOfMemory => return error.OutOfMemory,
         };
@@ -845,6 +849,7 @@ pub fn linkerUpdateFunc(pt: Zcu.PerThread, func_index: InternPool.Index, air: Ai
 
 /// https://github.com/ziglang/zig/issues/14307
 pub fn semaPkg(pt: Zcu.PerThread, pkg: *Module) !void {
+    dev.check(.sema);
     const import_file_result = try pt.importPkg(pkg);
     const root_decl_index = pt.zcu.fileRootDecl(import_file_result.file_index);
     if (root_decl_index == .none) {
@@ -2481,7 +2486,6 @@ fn processExportsInner(
     if (zcu.comp.bin_file) |lf| {
         try zcu.handleUpdateExports(export_indices, lf.updateExports(pt, exported, export_indices));
     } else if (zcu.llvm_object) |llvm_object| {
-        if (build_options.only_c) unreachable;
         try zcu.handleUpdateExports(export_indices, llvm_object.updateExports(pt, exported, export_indices));
     }
 }
@@ -2654,7 +2658,6 @@ pub fn linkerUpdateDecl(pt: Zcu.PerThread, decl_index: Zcu.Decl.Index) !void {
             },
         };
     } else if (zcu.llvm_object) |llvm_object| {
-        if (build_options.only_c) unreachable;
         llvm_object.updateDecl(pt, decl_index) catch |err| switch (err) {
             error.OutOfMemory => return error.OutOfMemory,
         };
@@ -3271,6 +3274,7 @@ const BigIntMutable = std.math.big.int.Mutable;
 const build_options = @import("build_options");
 const builtin = @import("builtin");
 const Cache = std.Build.Cache;
+const dev = @import("../dev.zig");
 const InternPool = @import("../InternPool.zig");
 const isUpDir = @import("../introspect.zig").isUpDir;
 const Liveness = @import("../Liveness.zig");
src/codegen.zig
@@ -22,6 +22,7 @@ const Type = @import("Type.zig");
 const Value = @import("Value.zig");
 const Zir = std.zig.Zir;
 const Alignment = InternPool.Alignment;
+const dev = @import("dev.zig");
 
 pub const Result = union(enum) {
     /// The `code` parameter passed to `generateSymbol` has the value ok.
@@ -43,6 +44,23 @@ pub const DebugInfoOutput = union(enum) {
     none,
 };
 
+fn devFeatureForBackend(comptime backend: std.builtin.CompilerBackend) dev.Feature {
+    comptime assert(mem.startsWith(u8, @tagName(backend), "stage2_"));
+    return @field(dev.Feature, @tagName(backend)["stage2_".len..] ++ "_backend");
+}
+
+fn importBackend(comptime backend: std.builtin.CompilerBackend) type {
+    return switch (backend) {
+        .stage2_aarch64 => @import("arch/aarch64/CodeGen.zig"),
+        .stage2_arm => @import("arch/arm/CodeGen.zig"),
+        .stage2_riscv64 => @import("arch/riscv64/CodeGen.zig"),
+        .stage2_sparc64 => @import("arch/sparc64/CodeGen.zig"),
+        .stage2_wasm => @import("arch/wasm/CodeGen.zig"),
+        .stage2_x86_64 => @import("arch/x86_64/CodeGen.zig"),
+        else => unreachable,
+    };
+}
+
 pub fn generateFunction(
     lf: *link.File,
     pt: Zcu.PerThread,
@@ -58,21 +76,18 @@ pub fn generateFunction(
     const decl = zcu.declPtr(func.owner_decl);
     const namespace = zcu.namespacePtr(decl.src_namespace);
     const target = namespace.fileScope(zcu).mod.resolved_target.result;
-    switch (target.cpu.arch) {
-        .arm,
-        .armeb,
-        => return @import("arch/arm/CodeGen.zig").generate(lf, pt, src_loc, func_index, air, liveness, code, debug_output),
-        .aarch64,
-        .aarch64_be,
-        .aarch64_32,
-        => return @import("arch/aarch64/CodeGen.zig").generate(lf, pt, src_loc, func_index, air, liveness, code, debug_output),
-        .riscv64 => return @import("arch/riscv64/CodeGen.zig").generate(lf, pt, src_loc, func_index, air, liveness, code, debug_output),
-        .sparc64 => return @import("arch/sparc64/CodeGen.zig").generate(lf, pt, src_loc, func_index, air, liveness, code, debug_output),
-        .x86_64 => return @import("arch/x86_64/CodeGen.zig").generate(lf, pt, src_loc, func_index, air, liveness, code, debug_output),
-        .wasm32,
-        .wasm64,
-        => return @import("arch/wasm/CodeGen.zig").generate(lf, pt, src_loc, func_index, air, liveness, code, debug_output),
+    switch (target_util.zigBackend(target, false)) {
         else => unreachable,
+        inline .stage2_aarch64,
+        .stage2_arm,
+        .stage2_riscv64,
+        .stage2_sparc64,
+        .stage2_wasm,
+        .stage2_x86_64,
+        => |backend| {
+            dev.check(devFeatureForBackend(backend));
+            return importBackend(backend).generate(lf, pt, src_loc, func_index, air, liveness, code, debug_output);
+        },
     }
 }
 
@@ -89,9 +104,12 @@ pub fn generateLazyFunction(
     const decl = zcu.declPtr(decl_index);
     const namespace = zcu.namespacePtr(decl.src_namespace);
     const target = namespace.fileScope(zcu).mod.resolved_target.result;
-    switch (target.cpu.arch) {
-        .x86_64 => return @import("arch/x86_64/CodeGen.zig").generateLazy(lf, pt, src_loc, lazy_sym, code, debug_output),
+    switch (target_util.zigBackend(target, false)) {
         else => unreachable,
+        inline .stage2_x86_64 => |backend| {
+            dev.check(devFeatureForBackend(backend));
+            return importBackend(backend).generateLazy(lf, pt, src_loc, lazy_sym, code, debug_output);
+        },
     }
 }
 
src/Compilation.zig
@@ -38,6 +38,7 @@ const Zir = std.zig.Zir;
 const Air = @import("Air.zig");
 const Builtin = @import("Builtin.zig");
 const LlvmObject = @import("codegen/llvm.zig").Object;
+const dev = @import("dev.zig");
 
 pub const Config = @import("Compilation/Config.zig");
 
@@ -94,8 +95,15 @@ native_system_include_paths: []const []const u8,
 force_undefined_symbols: std.StringArrayHashMapUnmanaged(void),
 
 c_object_table: std.AutoArrayHashMapUnmanaged(*CObject, void) = .{},
-win32_resource_table: if (build_options.only_core_functionality) void else std.AutoArrayHashMapUnmanaged(*Win32Resource, void) =
-    if (build_options.only_core_functionality) {} else .{},
+win32_resource_table: if (dev.env.supports(.win32_resource)) std.AutoArrayHashMapUnmanaged(*Win32Resource, void) else struct {
+    pub fn keys(_: @This()) [0]void {
+        return .{};
+    }
+    pub fn count(_: @This()) u0 {
+        return 0;
+    }
+    pub fn deinit(_: @This(), _: Allocator) void {}
+} = .{},
 
 link_error_flags: link.File.ErrorFlags = .{},
 link_errors: std.ArrayListUnmanaged(link.File.ErrorMsg) = .{},
@@ -125,7 +133,13 @@ c_object_work_queue: std.fifo.LinearFifo(*CObject, .Dynamic),
 
 /// These jobs are to invoke the RC compiler to create a compiled resource file (.res), which
 /// gets linked with the Compilation.
-win32_resource_work_queue: if (build_options.only_core_functionality) void else std.fifo.LinearFifo(*Win32Resource, .Dynamic),
+win32_resource_work_queue: if (dev.env.supports(.win32_resource)) std.fifo.LinearFifo(*Win32Resource, .Dynamic) else struct {
+    pub fn ensureUnusedCapacity(_: @This(), _: u0) error{}!void {}
+    pub fn readItem(_: @This()) ?noreturn {
+        return null;
+    }
+    pub fn deinit(_: @This()) void {}
+},
 
 /// These jobs are to tokenize, parse, and astgen files, which may be outdated
 /// since the last compilation, as well as scan for `@import` and queue up
@@ -142,8 +156,12 @@ failed_c_objects: std.AutoArrayHashMapUnmanaged(*CObject, *CObject.Diag.Bundle)
 
 /// The ErrorBundle memory is owned by the `Win32Resource`, using Compilation's general purpose allocator.
 /// This data is accessed by multiple threads and is protected by `mutex`.
-failed_win32_resources: if (build_options.only_core_functionality) void else std.AutoArrayHashMapUnmanaged(*Win32Resource, ErrorBundle) =
-    if (build_options.only_core_functionality) {} else .{},
+failed_win32_resources: if (dev.env.supports(.win32_resource)) std.AutoArrayHashMapUnmanaged(*Win32Resource, ErrorBundle) else struct {
+    pub fn values(_: @This()) [0]void {
+        return .{};
+    }
+    pub fn deinit(_: @This(), _: Allocator) void {}
+} = .{},
 
 /// Miscellaneous things that can fail.
 misc_failures: std.AutoArrayHashMapUnmanaged(MiscTask, MiscError) = .{},
@@ -1484,7 +1502,7 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
                 .done = false,
             },
             .c_object_work_queue = std.fifo.LinearFifo(*CObject, .Dynamic).init(gpa),
-            .win32_resource_work_queue = if (build_options.only_core_functionality) {} else std.fifo.LinearFifo(*Win32Resource, .Dynamic).init(gpa),
+            .win32_resource_work_queue = if (dev.env.supports(.win32_resource)) std.fifo.LinearFifo(*Win32Resource, .Dynamic).init(gpa) else .{},
             .astgen_work_queue = std.fifo.LinearFifo(Zcu.File.Index, .Dynamic).init(gpa),
             .embed_file_work_queue = std.fifo.LinearFifo(*Zcu.EmbedFile, .Dynamic).init(gpa),
             .c_source_files = options.c_source_files,
@@ -1711,7 +1729,7 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
             comp.emit_llvm_ir != null or
             comp.emit_llvm_bc != null))
         {
-            if (build_options.only_c) unreachable;
+            dev.check(.llvm_backend);
             if (opt_zcu) |zcu| zcu.llvm_object = try LlvmObject.create(arena, comp);
         }
 
@@ -1738,8 +1756,11 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
     }
 
     // Add a `Win32Resource` for each `rc_source_files` and one for `manifest_file`.
-    if (!build_options.only_core_functionality) {
-        try comp.win32_resource_table.ensureTotalCapacity(gpa, options.rc_source_files.len + @intFromBool(options.manifest_file != null));
+    const win32_resource_count =
+        options.rc_source_files.len + @intFromBool(options.manifest_file != null);
+    if (win32_resource_count > 0) {
+        dev.check(.win32_resource);
+        try comp.win32_resource_table.ensureTotalCapacity(gpa, win32_resource_count);
         for (options.rc_source_files) |rc_source_file| {
             const win32_resource = try gpa.create(Win32Resource);
             errdefer gpa.destroy(win32_resource);
@@ -1905,9 +1926,7 @@ pub fn destroy(comp: *Compilation) void {
     for (comp.work_queues) |work_queue| work_queue.deinit();
     if (!InternPool.single_threaded) comp.codegen_work.queue.deinit();
     comp.c_object_work_queue.deinit();
-    if (!build_options.only_core_functionality) {
-        comp.win32_resource_work_queue.deinit();
-    }
+    comp.win32_resource_work_queue.deinit();
     comp.astgen_work_queue.deinit();
     comp.embed_file_work_queue.deinit();
 
@@ -1956,17 +1975,15 @@ pub fn destroy(comp: *Compilation) void {
     }
     comp.failed_c_objects.deinit(gpa);
 
-    if (!build_options.only_core_functionality) {
-        for (comp.win32_resource_table.keys()) |key| {
-            key.destroy(gpa);
-        }
-        comp.win32_resource_table.deinit(gpa);
+    for (comp.win32_resource_table.keys()) |key| {
+        key.destroy(gpa);
+    }
+    comp.win32_resource_table.deinit(gpa);
 
-        for (comp.failed_win32_resources.values()) |*value| {
-            value.deinit(gpa);
-        }
-        comp.failed_win32_resources.deinit(gpa);
+    for (comp.failed_win32_resources.values()) |*value| {
+        value.deinit(gpa);
     }
+    comp.failed_win32_resources.deinit(gpa);
 
     for (comp.link_errors.items) |*item| item.deinit(gpa);
     comp.link_errors.deinit(gpa);
@@ -2153,17 +2170,15 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) !void {
 
     // For compiling Win32 resources, we rely on the cache hash system to avoid duplicating work.
     // Add a Job for each Win32 resource file.
-    if (!build_options.only_core_functionality) {
-        try comp.win32_resource_work_queue.ensureUnusedCapacity(comp.win32_resource_table.count());
-        for (comp.win32_resource_table.keys()) |key| {
-            comp.win32_resource_work_queue.writeItemAssumeCapacity(key);
-        }
-        if (comp.file_system_inputs) |fsi| {
-            for (comp.win32_resource_table.keys()) |win32_resource| switch (win32_resource.src) {
-                .rc => |f| try comp.appendFileSystemInput(fsi, Cache.Path.cwd(), f.src_path),
-                .manifest => continue,
-            };
-        }
+    try comp.win32_resource_work_queue.ensureUnusedCapacity(comp.win32_resource_table.count());
+    for (comp.win32_resource_table.keys()) |key| {
+        comp.win32_resource_work_queue.writeItemAssumeCapacity(key);
+    }
+    if (comp.file_system_inputs) |fsi| {
+        for (comp.win32_resource_table.keys()) |win32_resource| switch (win32_resource.src) {
+            .rc => |f| try comp.appendFileSystemInput(fsi, Cache.Path.cwd(), f.src_path),
+            .manifest => continue,
+        };
     }
 
     if (comp.module) |zcu| {
@@ -2397,7 +2412,6 @@ fn flush(comp: *Compilation, arena: Allocator, tid: Zcu.PerThread.Id, prog_node:
         try link.File.C.flushEmitH(zcu);
 
         if (zcu.llvm_object) |llvm_object| {
-            if (build_options.only_c) unreachable;
             const default_emit = switch (comp.cache_use) {
                 .whole => |whole| .{
                     .directory = whole.tmp_artifact_directory.?,
@@ -2541,17 +2555,15 @@ fn addNonIncrementalStuffToCacheManifest(
         man.hash.addListOfBytes(key.src.extra_flags);
     }
 
-    if (!build_options.only_core_functionality) {
-        for (comp.win32_resource_table.keys()) |key| {
-            switch (key.src) {
-                .rc => |rc_src| {
-                    _ = try man.addFile(rc_src.src_path, null);
-                    man.hash.addListOfBytes(rc_src.extra_flags);
-                },
-                .manifest => |manifest_path| {
-                    _ = try man.addFile(manifest_path, null);
-                },
-            }
+    for (comp.win32_resource_table.keys()) |key| {
+        switch (key.src) {
+            .rc => |rc_src| {
+                _ = try man.addFile(rc_src.src_path, null);
+                man.hash.addListOfBytes(rc_src.extra_flags);
+            },
+            .manifest => |manifest_path| {
+                _ = try man.addFile(manifest_path, null);
+            },
         }
     }
 
@@ -2695,8 +2707,6 @@ pub fn emitLlvmObject(
     llvm_object: *LlvmObject,
     prog_node: std.Progress.Node,
 ) !void {
-    if (build_options.only_c) @compileError("unreachable");
-
     const sub_prog_node = prog_node.start("LLVM Emit Object", 0);
     defer sub_prog_node.end();
 
@@ -2860,6 +2870,7 @@ fn reportMultiModuleErrors(pt: Zcu.PerThread) !void {
 /// or whatever is needed so that it can be executed.
 /// After this, one must call` makeFileWritable` before calling `update`.
 pub fn makeBinFileExecutable(comp: *Compilation) !void {
+    if (!dev.env.supports(.make_executable)) return;
     const lf = comp.bin_file orelse return;
     return lf.makeExecutable();
 }
@@ -2897,6 +2908,8 @@ const Header = extern struct {
 /// saved, such as the target and most CLI flags. A cache hit will only occur
 /// when subsequent compiler invocations use the same set of flags.
 pub fn saveState(comp: *Compilation) !void {
+    dev.check(.incremental);
+
     const lf = comp.bin_file orelse return;
 
     const gpa = comp.gpa;
@@ -3001,10 +3014,8 @@ pub fn totalErrorCount(comp: *Compilation) u32 {
         total += bundle.diags.len;
     }
 
-    if (!build_options.only_core_functionality) {
-        for (comp.failed_win32_resources.values()) |errs| {
-            total += errs.errorMessageCount();
-        }
+    for (comp.failed_win32_resources.values()) |errs| {
+        total += errs.errorMessageCount();
     }
 
     if (comp.module) |zcu| {
@@ -3082,10 +3093,8 @@ pub fn getAllErrorsAlloc(comp: *Compilation) !ErrorBundle {
         try diag_bundle.addToErrorBundle(&bundle);
     }
 
-    if (!build_options.only_core_functionality) {
-        for (comp.failed_win32_resources.values()) |error_bundle| {
-            try bundle.addBundleAsRoots(error_bundle);
-        }
+    for (comp.failed_win32_resources.values()) |error_bundle| {
+        try bundle.addBundleAsRoots(error_bundle);
     }
 
     for (comp.lld_errors.items) |lld_error| {
@@ -3509,11 +3518,10 @@ fn performAllTheWorkInner(
     comp.work_queue_wait_group.reset();
     defer comp.work_queue_wait_group.wait();
 
-    if (!build_options.only_c and !build_options.only_core_functionality) {
-        if (comp.docs_emit != null) {
-            comp.thread_pool.spawnWg(&comp.work_queue_wait_group, workerDocsCopy, .{comp});
-            comp.work_queue_wait_group.spawnManager(workerDocsWasm, .{ comp, main_progress_node });
-        }
+    if (comp.docs_emit != null) {
+        dev.check(.docs_emit);
+        comp.thread_pool.spawnWg(&comp.work_queue_wait_group, workerDocsCopy, .{comp});
+        comp.work_queue_wait_group.spawnManager(workerDocsWasm, .{ comp, main_progress_node });
     }
 
     {
@@ -3585,12 +3593,10 @@ fn performAllTheWorkInner(
             });
         }
 
-        if (!build_options.only_core_functionality) {
-            while (comp.win32_resource_work_queue.readItem()) |win32_resource| {
-                comp.thread_pool.spawnWg(&comp.work_queue_wait_group, workerUpdateWin32Resource, .{
-                    comp, win32_resource, main_progress_node,
-                });
-            }
+        while (comp.win32_resource_work_queue.readItem()) |win32_resource| {
+            comp.thread_pool.spawnWg(&comp.work_queue_wait_group, workerUpdateWin32Resource, .{
+                comp, win32_resource, main_progress_node,
+            });
         }
     }
 
@@ -3867,9 +3873,6 @@ fn processOneJob(tid: usize, comp: *Compilation, job: Job, prog_node: std.Progre
             };
         },
         .windows_import_lib => |index| {
-            if (build_options.only_c)
-                @panic("building import libs not included in core functionality");
-
             const named_frame = tracy.namedFrame("windows_import_lib");
             defer named_frame.end();
 
@@ -4466,7 +4469,8 @@ pub const CImportResult = struct {
 /// This API is currently coupled pretty tightly to stage1's needs; it will need to be reworked
 /// a bit when we want to start using it from self-hosted.
 pub fn cImport(comp: *Compilation, c_src: []const u8, owner_mod: *Package.Module) !CImportResult {
-    if (build_options.only_core_functionality) @panic("@cImport is not available in a zig2.c build");
+    dev.check(.translate_c_command);
+
     const tracy_trace = trace(@src());
     defer tracy_trace.end();
 
src/dev.zig
@@ -0,0 +1,238 @@
+pub const Env = enum {
+    /// zig1 features
+    bootstrap,
+
+    /// zig2 features
+    core,
+
+    /// stage3 features
+    full,
+
+    /// - `zig cc`
+    /// - `zig c++`
+    /// - `zig translate-c`
+    c_source,
+
+    /// - `zig ast-check`
+    /// - `zig changelist`
+    /// - `zig dump-zir`
+    ast_gen,
+
+    /// - ast_gen
+    /// - `zig build-* -fno-emit-bin`
+    sema,
+
+    /// - sema
+    /// - jit command on x86_64-linux host
+    /// - `zig build-* -fno-llvm -fno-lld -target x86_64-linux`
+    @"x86_64-linux",
+
+    pub inline fn supports(comptime dev_env: Env, comptime feature: Feature) bool {
+        return switch (dev_env) {
+            .full => true,
+            .bootstrap => switch (feature) {
+                .build_exe_command,
+                .build_obj_command,
+                .ast_gen,
+                .sema,
+                .c_backend,
+                .c_linker,
+                => true,
+                else => false,
+            },
+            .core => switch (feature) {
+                .build_exe_command,
+                .build_lib_command,
+                .build_obj_command,
+                .test_command,
+                .run_command,
+                .ar_command,
+                .build_command,
+                .clang_command,
+                .stdio_listen,
+                .build_import_lib,
+                .make_executable,
+                .make_writable,
+                .incremental,
+                .ast_gen,
+                .sema,
+                .llvm_backend,
+                .c_backend,
+                .wasm_backend,
+                .arm_backend,
+                .x86_64_backend,
+                .aarch64_backend,
+                .x86_backend,
+                .riscv64_backend,
+                .sparc64_backend,
+                .spirv64_backend,
+                .lld_linker,
+                .coff_linker,
+                .elf_linker,
+                .macho_linker,
+                .c_linker,
+                .wasm_linker,
+                .spirv_linker,
+                .plan9_linker,
+                .nvptx_linker,
+                => true,
+                .cc_command,
+                .translate_c_command,
+                .jit_command,
+                .fetch_command,
+                .init_command,
+                .targets_command,
+                .version_command,
+                .env_command,
+                .zen_command,
+                .help_command,
+                .ast_check_command,
+                .detect_cpu_command,
+                .changelist_command,
+                .dump_zir_command,
+                .llvm_ints_command,
+                .docs_emit,
+                // Avoid dragging networking into zig2.c because it adds dependencies on some
+                // linker symbols that are annoying to satisfy while bootstrapping.
+                .network_listen,
+                .win32_resource,
+                => false,
+            },
+            .c_source => switch (feature) {
+                .clang_command,
+                .cc_command,
+                .translate_c_command,
+                => true,
+                else => false,
+            },
+            .ast_gen => switch (feature) {
+                .ast_check_command,
+                .changelist_command,
+                .dump_zir_command,
+                .make_executable,
+                .make_writable,
+                .incremental,
+                .ast_gen,
+                => true,
+                else => false,
+            },
+            .sema => switch (feature) {
+                .build_exe_command,
+                .build_lib_command,
+                .build_obj_command,
+                .test_command,
+                .run_command,
+                .sema,
+                => true,
+                else => Env.ast_gen.supports(feature),
+            },
+            .@"x86_64-linux" => switch (feature) {
+                .x86_64_backend,
+                .elf_linker,
+                => true,
+                else => Env.sema.supports(feature),
+            },
+        };
+    }
+
+    pub inline fn supportsAny(comptime dev_env: Env, comptime features: []const Feature) bool {
+        inline for (features) |feature| if (dev_env.supports(feature)) return true;
+        return false;
+    }
+
+    pub inline fn supportsAll(comptime dev_env: Env, comptime features: []const Feature) bool {
+        inline for (features) |feature| if (!dev_env.supports(feature)) return false;
+        return true;
+    }
+};
+
+pub const Feature = enum {
+    build_exe_command,
+    build_lib_command,
+    build_obj_command,
+    test_command,
+    run_command,
+    ar_command,
+    build_command,
+    clang_command,
+    cc_command,
+    translate_c_command,
+    jit_command,
+    fetch_command,
+    init_command,
+    targets_command,
+    version_command,
+    env_command,
+    zen_command,
+    help_command,
+    ast_check_command,
+    detect_cpu_command,
+    changelist_command,
+    dump_zir_command,
+    llvm_ints_command,
+
+    docs_emit,
+    stdio_listen,
+    network_listen,
+    build_import_lib,
+    win32_resource,
+    make_executable,
+    make_writable,
+    incremental,
+    ast_gen,
+    sema,
+
+    llvm_backend,
+    c_backend,
+    wasm_backend,
+    arm_backend,
+    x86_64_backend,
+    aarch64_backend,
+    x86_backend,
+    riscv64_backend,
+    sparc64_backend,
+    spirv64_backend,
+
+    lld_linker,
+    coff_linker,
+    elf_linker,
+    macho_linker,
+    c_linker,
+    wasm_linker,
+    spirv_linker,
+    plan9_linker,
+    nvptx_linker,
+};
+
+/// Makes the code following the call to this function unreachable if `feature` is disabled.
+pub fn check(comptime feature: Feature) if (env.supports(feature)) void else noreturn {
+    if (env.supports(feature)) return;
+    @panic("development environment " ++ @tagName(env) ++ " does not support feature " ++ @tagName(feature));
+}
+
+/// Makes the code following the call to this function unreachable if all of `features` are disabled.
+pub fn checkAny(comptime features: []const Feature) if (env.supportsAny(features)) void else noreturn {
+    if (env.supportsAny(features)) return;
+    comptime var feature_tags: []const u8 = "";
+    inline for (features[0 .. features.len - 1]) |feature| feature_tags = feature_tags ++ @tagName(feature) ++ ", ";
+    feature_tags = feature_tags ++ "or " ++ @tagName(features[features.len - 1]);
+    @panic("development environment " ++ @tagName(env) ++ " does not support feature " ++ feature_tags);
+}
+
+/// Makes the code following the call to this function unreachable if any of `features` are disabled.
+pub fn checkAll(comptime features: []const Feature) if (env.supportsAll(features)) void else noreturn {
+    if (env.supportsAll(features)) return;
+    inline for (features) |feature| if (!env.supports(feature))
+        @panic("development environment " ++ @tagName(env) ++ " does not support feature " ++ @tagName(feature));
+}
+
+const build_options = @import("build_options");
+
+pub const env: Env = if (@hasDecl(build_options, "dev"))
+    @field(Env, @tagName(build_options.dev))
+else if (@hasDecl(build_options, "only_c") and build_options.only_c)
+    .bootstrap
+else if (@hasDecl(build_options, "only_core_functionality") and build_options.only_core_functionality)
+    .core
+else
+    .full;
src/link.zig
@@ -21,6 +21,7 @@ const Value = @import("Value.zig");
 const LlvmObject = @import("codegen/llvm.zig").Object;
 const lldMain = @import("main.zig").lldMain;
 const Package = @import("Package.zig");
+const dev = @import("dev.zig");
 
 /// When adding a new field, remember to update `hashAddSystemLibs`.
 /// These are *always* dynamically linked. Static libraries will be
@@ -192,7 +193,7 @@ pub const File = struct {
     ) !*File {
         switch (Tag.fromObjectFormat(comp.root_mod.resolved_target.result.ofmt)) {
             inline else => |tag| {
-                if (tag != .c and build_options.only_c) unreachable;
+                dev.check(tag.devFeature());
                 const ptr = try tag.Type().open(arena, comp, emit, options);
                 return &ptr.base;
             },
@@ -207,7 +208,7 @@ pub const File = struct {
     ) !*File {
         switch (Tag.fromObjectFormat(comp.root_mod.resolved_target.result.ofmt)) {
             inline else => |tag| {
-                if (tag != .c and build_options.only_c) unreachable;
+                dev.check(tag.devFeature());
                 const ptr = try tag.Type().createEmpty(arena, comp, emit, options);
                 return &ptr.base;
             },
@@ -219,12 +220,13 @@ pub const File = struct {
     }
 
     pub fn makeWritable(base: *File) !void {
+        dev.check(.make_writable);
         const comp = base.comp;
         const gpa = comp.gpa;
         switch (base.tag) {
             .coff, .elf, .macho, .plan9, .wasm => {
-                if (build_options.only_c) unreachable;
                 if (base.file != null) return;
+                dev.checkAny(&.{ .coff_linker, .elf_linker, .macho_linker, .plan9_linker, .wasm_linker });
                 const emit = base.emit;
                 if (base.child_pid) |pid| {
                     if (builtin.os.tag == .windows) {
@@ -263,11 +265,12 @@ pub const File = struct {
                     .mode = determineMode(use_lld, output_mode, link_mode),
                 });
             },
-            .c, .spirv, .nvptx => {},
+            .c, .spirv, .nvptx => dev.checkAny(&.{ .c_linker, .spirv_linker, .nvptx_linker }),
         }
     }
 
     pub fn makeExecutable(base: *File) !void {
+        dev.check(.make_executable);
         const comp = base.comp;
         const output_mode = comp.config.output_mode;
         const link_mode = comp.config.link_mode;
@@ -283,7 +286,7 @@ pub const File = struct {
         }
         switch (base.tag) {
             .elf => if (base.file) |f| {
-                if (build_options.only_c) unreachable;
+                dev.check(.elf_linker);
                 if (base.zcu_object_sub_path != 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.
@@ -302,7 +305,7 @@ pub const File = struct {
                 }
             },
             .coff, .macho, .plan9, .wasm => if (base.file) |f| {
-                if (build_options.only_c) unreachable;
+                dev.checkAny(&.{ .coff_linker, .macho_linker, .plan9_linker, .wasm_linker });
                 if (base.zcu_object_sub_path != 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.
@@ -321,7 +324,7 @@ pub const File = struct {
                     }
                 }
             },
-            .c, .spirv, .nvptx => {},
+            .c, .spirv, .nvptx => dev.checkAny(&.{ .c_linker, .spirv_linker, .nvptx_linker }),
         }
     }
 
@@ -366,13 +369,13 @@ pub const File = struct {
     /// constant. Returns the symbol index of the lowered constant in the read-only section
     /// of the final binary.
     pub fn lowerUnnamedConst(base: *File, pt: Zcu.PerThread, val: Value, decl_index: InternPool.DeclIndex) UpdateDeclError!u32 {
-        if (build_options.only_c) @compileError("unreachable");
         switch (base.tag) {
             .spirv => unreachable,
             .c => unreachable,
             .nvptx => unreachable,
-            inline else => |t| {
-                return @as(*t.Type(), @fieldParentPtr("base", base)).lowerUnnamedConst(pt, val, decl_index);
+            inline else => |tag| {
+                dev.check(tag.devFeature());
+                return @as(*tag.Type(), @fieldParentPtr("base", base)).lowerUnnamedConst(pt, val, decl_index);
             },
         }
     }
@@ -383,15 +386,15 @@ pub const File = struct {
     /// Optionally, it is possible to specify where to expect the symbol defined if it
     /// is an import.
     pub fn getGlobalSymbol(base: *File, name: []const u8, lib_name: ?[]const u8) UpdateDeclError!u32 {
-        if (build_options.only_c) @compileError("unreachable");
         log.debug("getGlobalSymbol '{s}' (expected in '{?s}')", .{ name, lib_name });
         switch (base.tag) {
             .plan9 => unreachable,
             .spirv => unreachable,
             .c => unreachable,
             .nvptx => unreachable,
-            inline else => |t| {
-                return @as(*t.Type(), @fieldParentPtr("base", base)).getGlobalSymbol(name, lib_name);
+            inline else => |tag| {
+                dev.check(tag.devFeature());
+                return @as(*tag.Type(), @fieldParentPtr("base", base)).getGlobalSymbol(name, lib_name);
             },
         }
     }
@@ -402,7 +405,7 @@ pub const File = struct {
         assert(decl.has_tv);
         switch (base.tag) {
             inline else => |tag| {
-                if (tag != .c and build_options.only_c) unreachable;
+                dev.check(tag.devFeature());
                 return @as(*tag.Type(), @fieldParentPtr("base", base)).updateDecl(pt, decl_index);
             },
         }
@@ -418,7 +421,7 @@ pub const File = struct {
     ) UpdateDeclError!void {
         switch (base.tag) {
             inline else => |tag| {
-                if (tag != .c and build_options.only_c) unreachable;
+                dev.check(tag.devFeature());
                 return @as(*tag.Type(), @fieldParentPtr("base", base)).updateFunc(pt, func_index, air, liveness);
             },
         }
@@ -430,7 +433,7 @@ pub const File = struct {
         switch (base.tag) {
             .spirv, .nvptx => {},
             inline else => |tag| {
-                if (tag != .c and build_options.only_c) unreachable;
+                dev.check(tag.devFeature());
                 return @as(*tag.Type(), @fieldParentPtr("base", base)).updateDeclLineNumber(pt, decl_index);
             },
         }
@@ -454,7 +457,7 @@ pub const File = struct {
         if (base.file) |f| f.close();
         switch (base.tag) {
             inline else => |tag| {
-                if (tag != .c and build_options.only_c) unreachable;
+                dev.check(tag.devFeature());
                 @as(*tag.Type(), @fieldParentPtr("base", base)).deinit();
             },
         }
@@ -536,12 +539,9 @@ pub const File = struct {
     /// and `use_lld`, not only `effectiveOutputMode`.
     /// `arena` has the lifetime of the call to `Compilation.update`.
     pub fn flush(base: *File, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) FlushError!void {
-        if (build_options.only_c) {
-            assert(base.tag == .c);
-            return @as(*C, @fieldParentPtr("base", base)).flush(arena, tid, prog_node);
-        }
         const comp = base.comp;
         if (comp.clang_preprocessor_mode == .yes or comp.clang_preprocessor_mode == .pch) {
+            dev.check(.clang_command);
             const gpa = comp.gpa;
             const emit = base.emit;
             // TODO: avoid extra link step when it's just 1 object file (the `zig cc -c` case)
@@ -565,6 +565,7 @@ pub const File = struct {
         }
         switch (base.tag) {
             inline else => |tag| {
+                dev.check(tag.devFeature());
                 return @as(*tag.Type(), @fieldParentPtr("base", base)).flush(arena, tid, prog_node);
             },
         }
@@ -575,7 +576,7 @@ pub const File = struct {
     pub fn flushModule(base: *File, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) FlushError!void {
         switch (base.tag) {
             inline else => |tag| {
-                if (tag != .c and build_options.only_c) unreachable;
+                dev.check(tag.devFeature());
                 return @as(*tag.Type(), @fieldParentPtr("base", base)).flushModule(arena, tid, prog_node);
             },
         }
@@ -585,7 +586,7 @@ pub const File = struct {
     pub fn freeDecl(base: *File, decl_index: InternPool.DeclIndex) void {
         switch (base.tag) {
             inline else => |tag| {
-                if (tag != .c and build_options.only_c) unreachable;
+                dev.check(tag.devFeature());
                 @as(*tag.Type(), @fieldParentPtr("base", base)).freeDecl(decl_index);
             },
         }
@@ -608,7 +609,7 @@ pub const File = struct {
     ) UpdateExportsError!void {
         switch (base.tag) {
             inline else => |tag| {
-                if (tag != .c and build_options.only_c) unreachable;
+                dev.check(tag.devFeature());
                 return @as(*tag.Type(), @fieldParentPtr("base", base)).updateExports(pt, exported, export_indices);
             },
         }
@@ -627,12 +628,12 @@ pub const File = struct {
     /// May be called before or after updateFunc/updateDecl therefore it is up to the linker to allocate
     /// the block/atom.
     pub fn getDeclVAddr(base: *File, pt: Zcu.PerThread, decl_index: InternPool.DeclIndex, reloc_info: RelocInfo) !u64 {
-        if (build_options.only_c) @compileError("unreachable");
         switch (base.tag) {
             .c => unreachable,
             .spirv => unreachable,
             .nvptx => unreachable,
             inline else => |tag| {
+                dev.check(tag.devFeature());
                 return @as(*tag.Type(), @fieldParentPtr("base", base)).getDeclVAddr(pt, decl_index, reloc_info);
             },
         }
@@ -647,24 +648,24 @@ pub const File = struct {
         decl_align: InternPool.Alignment,
         src_loc: Zcu.LazySrcLoc,
     ) !LowerResult {
-        if (build_options.only_c) @compileError("unreachable");
         switch (base.tag) {
             .c => unreachable,
             .spirv => unreachable,
             .nvptx => unreachable,
             inline else => |tag| {
+                dev.check(tag.devFeature());
                 return @as(*tag.Type(), @fieldParentPtr("base", base)).lowerAnonDecl(pt, decl_val, decl_align, src_loc);
             },
         }
     }
 
     pub fn getAnonDeclVAddr(base: *File, decl_val: InternPool.Index, reloc_info: RelocInfo) !u64 {
-        if (build_options.only_c) @compileError("unreachable");
         switch (base.tag) {
             .c => unreachable,
             .spirv => unreachable,
             .nvptx => unreachable,
             inline else => |tag| {
+                dev.check(tag.devFeature());
                 return @as(*tag.Type(), @fieldParentPtr("base", base)).getAnonDeclVAddr(decl_val, reloc_info);
             },
         }
@@ -675,7 +676,6 @@ pub const File = struct {
         exported: Zcu.Exported,
         name: InternPool.NullTerminatedString,
     ) void {
-        if (build_options.only_c) @compileError("unreachable");
         switch (base.tag) {
             .plan9,
             .spirv,
@@ -683,12 +683,15 @@ pub const File = struct {
             => {},
 
             inline else => |tag| {
+                dev.check(tag.devFeature());
                 return @as(*tag.Type(), @fieldParentPtr("base", base)).deleteExport(exported, name);
             },
         }
     }
 
     pub fn linkAsArchive(base: *File, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) FlushError!void {
+        dev.check(.lld_linker);
+
         const tracy = trace(@src());
         defer tracy.end();
 
@@ -743,10 +746,8 @@ pub const File = struct {
             for (comp.c_object_table.keys()) |key| {
                 _ = try man.addFile(key.status.success.object_path, null);
             }
-            if (!build_options.only_core_functionality) {
-                for (comp.win32_resource_table.keys()) |key| {
-                    _ = try man.addFile(key.status.success.res_path, null);
-                }
+            for (comp.win32_resource_table.keys()) |key| {
+                _ = try man.addFile(key.status.success.res_path, null);
             }
             try man.addOptionalFile(zcu_obj_path);
             try man.addOptionalFile(compiler_rt_path);
@@ -777,7 +778,7 @@ pub const File = struct {
             };
         }
 
-        const win32_resource_table_len = if (build_options.only_core_functionality) 0 else comp.win32_resource_table.count();
+        const win32_resource_table_len = comp.win32_resource_table.count();
         const num_object_files = objects.len + comp.c_object_table.count() + win32_resource_table_len + 2;
         var object_files = try std.ArrayList([*:0]const u8).initCapacity(gpa, num_object_files);
         defer object_files.deinit();
@@ -788,10 +789,8 @@ pub const File = struct {
         for (comp.c_object_table.keys()) |key| {
             object_files.appendAssumeCapacity(try arena.dupeZ(u8, key.status.success.object_path));
         }
-        if (!build_options.only_core_functionality) {
-            for (comp.win32_resource_table.keys()) |key| {
-                object_files.appendAssumeCapacity(try arena.dupeZ(u8, key.status.success.res_path));
-            }
+        for (comp.win32_resource_table.keys()) |key| {
+            object_files.appendAssumeCapacity(try arena.dupeZ(u8, key.status.success.res_path));
         }
         if (zcu_obj_path) |p| {
             object_files.appendAssumeCapacity(try arena.dupeZ(u8, p));
@@ -869,6 +868,10 @@ pub const File = struct {
                 .dxcontainer => @panic("TODO implement dxcontainer object format"),
             };
         }
+
+        pub fn devFeature(tag: Tag) dev.Feature {
+            return @field(dev.Feature, @tagName(tag) ++ "_linker");
+        }
     };
 
     pub const ErrorFlags = struct {
src/main.zig
@@ -30,6 +30,7 @@ const Zcu = @import("Zcu.zig");
 const AstGen = std.zig.AstGen;
 const mingw = @import("mingw.zig");
 const Server = std.zig.Server;
+const dev = @import("dev.zig");
 
 pub const std_options = .{
     .wasiCwd = wasi_cwd,
@@ -195,17 +196,6 @@ pub fn main() anyerror!void {
         wasi_preopens = try fs.wasi.preopensAlloc(arena);
     }
 
-    // Short circuit some of the other logic for bootstrapping.
-    if (build_options.only_c) {
-        if (mem.eql(u8, args[1], "build-exe")) {
-            return buildOutputType(gpa, arena, args, .{ .build = .Exe });
-        } else if (mem.eql(u8, args[1], "build-obj")) {
-            return buildOutputType(gpa, arena, args, .{ .build = .Obj });
-        } else {
-            @panic("only build-exe or build-obj is supported in a -Donly-c build");
-        }
-    }
-
     return mainArgs(gpa, arena, args);
 }
 
@@ -227,6 +217,7 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
     }
 
     if (process.can_execv and std.posix.getenvZ("ZIG_IS_DETECTING_LIBC_PATHS") != null) {
+        dev.check(.cc_command);
         // In this case we have accidentally invoked ourselves as "the system C compiler"
         // to figure out where libc is installed. This is essentially infinite recursion
         // via child process execution due to the CC environment variable pointing to Zig.
@@ -260,39 +251,49 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
     const cmd = args[1];
     const cmd_args = args[2..];
     if (mem.eql(u8, cmd, "build-exe")) {
+        dev.check(.build_exe_command);
         return buildOutputType(gpa, arena, args, .{ .build = .Exe });
     } else if (mem.eql(u8, cmd, "build-lib")) {
+        dev.check(.build_lib_command);
         return buildOutputType(gpa, arena, args, .{ .build = .Lib });
     } else if (mem.eql(u8, cmd, "build-obj")) {
+        dev.check(.build_obj_command);
         return buildOutputType(gpa, arena, args, .{ .build = .Obj });
     } else if (mem.eql(u8, cmd, "test")) {
+        dev.check(.test_command);
         return buildOutputType(gpa, arena, args, .zig_test);
     } else if (mem.eql(u8, cmd, "run")) {
+        dev.check(.run_command);
         return buildOutputType(gpa, arena, args, .run);
     } else if (mem.eql(u8, cmd, "dlltool") or
         mem.eql(u8, cmd, "ranlib") or
         mem.eql(u8, cmd, "lib") or
         mem.eql(u8, cmd, "ar"))
     {
+        dev.check(.ar_command);
         return process.exit(try llvmArMain(arena, args));
     } else if (mem.eql(u8, cmd, "build")) {
+        dev.check(.build_command);
         return cmdBuild(gpa, arena, cmd_args);
     } else if (mem.eql(u8, cmd, "clang") or
         mem.eql(u8, cmd, "-cc1") or mem.eql(u8, cmd, "-cc1as"))
     {
+        dev.check(.clang_command);
         return process.exit(try clangMain(arena, args));
     } else if (mem.eql(u8, cmd, "ld.lld") or
         mem.eql(u8, cmd, "lld-link") or
         mem.eql(u8, cmd, "wasm-ld"))
     {
+        dev.check(.lld_linker);
         return process.exit(try lldMain(arena, args, true));
-    } else if (build_options.only_core_functionality) {
-        @panic("only a few subcommands are supported in a zig2.c build");
     } else if (mem.eql(u8, cmd, "cc")) {
+        dev.check(.cc_command);
         return buildOutputType(gpa, arena, args, .cc);
     } else if (mem.eql(u8, cmd, "c++")) {
+        dev.check(.cc_command);
         return buildOutputType(gpa, arena, args, .cpp);
     } else if (mem.eql(u8, cmd, "translate-c")) {
+        dev.check(.translate_c_command);
         return buildOutputType(gpa, arena, args, .translate_c);
     } else if (mem.eql(u8, cmd, "rc")) {
         const use_server = cmd_args.len > 0 and std.mem.eql(u8, cmd_args[0], "--zig-integration");
@@ -332,16 +333,19 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
     } else if (mem.eql(u8, cmd, "init")) {
         return cmdInit(gpa, arena, cmd_args);
     } else if (mem.eql(u8, cmd, "targets")) {
+        dev.check(.targets_command);
         const host = std.zig.resolveTargetQueryOrFatal(.{});
         const stdout = io.getStdOut().writer();
         return @import("print_targets.zig").cmdTargets(arena, cmd_args, stdout, host);
     } else if (mem.eql(u8, cmd, "version")) {
+        dev.check(.version_command);
         try std.io.getStdOut().writeAll(build_options.version ++ "\n");
         // Check libc++ linkage to make sure Zig was built correctly, but only
         // for "env" and "version" to avoid affecting the startup time for
         // build-critical commands (check takes about ~10 μs)
         return verifyLibcxxCorrectlyLinked();
     } else if (mem.eql(u8, cmd, "env")) {
+        dev.check(.env_command);
         verifyLibcxxCorrectlyLinked();
         return @import("print_env.zig").cmdEnv(arena, cmd_args, io.getStdOut().writer());
     } else if (mem.eql(u8, cmd, "reduce")) {
@@ -350,8 +354,10 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
             .root_src_path = "reduce.zig",
         });
     } else if (mem.eql(u8, cmd, "zen")) {
+        dev.check(.zen_command);
         return io.getStdOut().writeAll(info_zen);
     } else if (mem.eql(u8, cmd, "help") or mem.eql(u8, cmd, "-h") or mem.eql(u8, cmd, "--help")) {
+        dev.check(.help_command);
         return io.getStdOut().writeAll(usage);
     } else if (mem.eql(u8, cmd, "ast-check")) {
         return cmdAstCheck(gpa, arena, cmd_args);
@@ -726,14 +732,10 @@ const ArgMode = union(enum) {
     run,
 };
 
-/// Avoid dragging networking into zig2.c because it adds dependencies on some
-/// linker symbols that are annoying to satisfy while bootstrapping.
-const Ip4Address = if (build_options.only_core_functionality) void else std.net.Ip4Address;
-
 const Listen = union(enum) {
     none,
-    ip4: Ip4Address,
-    stdio,
+    stdio: if (dev.env.supports(.stdio_listen)) void else noreturn,
+    ip4: if (dev.env.supports(.network_listen)) std.net.Ip4Address else noreturn,
 };
 
 const ArgsIterator = struct {
@@ -1338,9 +1340,10 @@ fn buildOutputType(
                     } else if (mem.eql(u8, arg, "--listen")) {
                         const next_arg = args_iter.nextOrFatal();
                         if (mem.eql(u8, next_arg, "-")) {
+                            dev.check(.stdio_listen);
                             listen = .stdio;
                         } else {
-                            if (build_options.only_core_functionality) unreachable;
+                            dev.check(.network_listen);
                             // example: --listen 127.0.0.1:9000
                             var it = std.mem.splitScalar(u8, next_arg, ':');
                             const host = it.next().?;
@@ -1351,6 +1354,7 @@ fn buildOutputType(
                                 fatal("invalid host: '{s}': {s}", .{ host, @errorName(err) }) };
                         }
                     } else if (mem.eql(u8, arg, "--listen=-")) {
+                        dev.check(.stdio_listen);
                         listen = .stdio;
                     } else if (mem.eql(u8, arg, "--debug-link-snapshot")) {
                         if (!build_options.enable_link_snapshots) {
@@ -1359,6 +1363,7 @@ fn buildOutputType(
                             enable_link_snapshots = true;
                         }
                     } else if (mem.eql(u8, arg, "-fincremental")) {
+                        dev.check(.incremental);
                         opt_incremental = true;
                     } else if (mem.eql(u8, arg, "-fno-incremental")) {
                         opt_incremental = false;
@@ -1762,7 +1767,7 @@ fn buildOutputType(
             }
         },
         .cc, .cpp => {
-            if (build_options.only_c) unreachable;
+            dev.check(.cc_command);
 
             emit_h = .no;
             soname = .no;
@@ -3395,7 +3400,6 @@ fn buildOutputType(
     switch (listen) {
         .none => {},
         .stdio => {
-            if (build_options.only_c) unreachable;
             try serve(
                 comp,
                 std.io.getStdIn(),
@@ -3409,8 +3413,6 @@ fn buildOutputType(
             return cleanExit();
         },
         .ip4 => |ip4_addr| {
-            if (build_options.only_core_functionality) unreachable;
-
             const addr: std.net.Address = .{ .in = ip4_addr };
 
             var server = try addr.listen(.{
@@ -3454,50 +3456,50 @@ fn buildOutputType(
             else => |e| return e,
         };
     }
-    if (build_options.only_c) return cleanExit();
     try comp.makeBinFileExecutable();
     saveState(comp, incremental);
 
-    if (test_exec_args.items.len == 0 and target.ofmt == .c) default_exec_args: {
-        // Default to using `zig run` to execute the produced .c code from `zig test`.
-        const c_code_loc = emit_bin_loc orelse break :default_exec_args;
-        const c_code_directory = c_code_loc.directory orelse comp.bin_file.?.emit.directory;
-        const c_code_path = try fs.path.join(arena, &[_][]const u8{
-            c_code_directory.path orelse ".", c_code_loc.basename,
-        });
-        try test_exec_args.appendSlice(arena, &.{ self_exe_path, "run" });
-        if (zig_lib_directory.path) |p| {
-            try test_exec_args.appendSlice(arena, &.{ "-I", p });
-        }
-
-        if (create_module.resolved_options.link_libc) {
-            try test_exec_args.append(arena, "-lc");
-        } else if (target.os.tag == .windows) {
-            try test_exec_args.appendSlice(arena, &.{
-                "--subsystem", "console",
-                "-lkernel32",  "-lntdll",
+    if (switch (arg_mode) {
+        .run => true,
+        .zig_test => !test_no_exec,
+        else => false,
+    }) {
+        dev.checkAny(&.{ .run_command, .test_command });
+
+        if (test_exec_args.items.len == 0 and target.ofmt == .c) default_exec_args: {
+            // Default to using `zig run` to execute the produced .c code from `zig test`.
+            const c_code_loc = emit_bin_loc orelse break :default_exec_args;
+            const c_code_directory = c_code_loc.directory orelse comp.bin_file.?.emit.directory;
+            const c_code_path = try fs.path.join(arena, &[_][]const u8{
+                c_code_directory.path orelse ".", c_code_loc.basename,
             });
-        }
+            try test_exec_args.appendSlice(arena, &.{ self_exe_path, "run" });
+            if (zig_lib_directory.path) |p| {
+                try test_exec_args.appendSlice(arena, &.{ "-I", p });
+            }
 
-        const first_cli_mod = create_module.modules.values()[0];
-        if (first_cli_mod.target_arch_os_abi) |triple| {
-            try test_exec_args.appendSlice(arena, &.{ "-target", triple });
-        }
-        if (first_cli_mod.target_mcpu) |mcpu| {
-            try test_exec_args.append(arena, try std.fmt.allocPrint(arena, "-mcpu={s}", .{mcpu}));
-        }
-        if (create_module.dynamic_linker) |dl| {
-            try test_exec_args.appendSlice(arena, &.{ "--dynamic-linker", dl });
+            if (create_module.resolved_options.link_libc) {
+                try test_exec_args.append(arena, "-lc");
+            } else if (target.os.tag == .windows) {
+                try test_exec_args.appendSlice(arena, &.{
+                    "--subsystem", "console",
+                    "-lkernel32",  "-lntdll",
+                });
+            }
+
+            const first_cli_mod = create_module.modules.values()[0];
+            if (first_cli_mod.target_arch_os_abi) |triple| {
+                try test_exec_args.appendSlice(arena, &.{ "-target", triple });
+            }
+            if (first_cli_mod.target_mcpu) |mcpu| {
+                try test_exec_args.append(arena, try std.fmt.allocPrint(arena, "-mcpu={s}", .{mcpu}));
+            }
+            if (create_module.dynamic_linker) |dl| {
+                try test_exec_args.appendSlice(arena, &.{ "--dynamic-linker", dl });
+            }
+            try test_exec_args.append(arena, c_code_path);
         }
-        try test_exec_args.append(arena, c_code_path);
-    }
 
-    const run_or_test = switch (arg_mode) {
-        .run => true,
-        .zig_test => !test_no_exec,
-        else => false,
-    };
-    if (run_or_test) {
         try runOrTest(
             comp,
             gpa,
@@ -4459,7 +4461,8 @@ fn cmdTranslateC(
     file_system_inputs: ?*std.ArrayListUnmanaged(u8),
     prog_node: std.Progress.Node,
 ) !void {
-    if (build_options.only_core_functionality) @panic("@translate-c is not available in a zig2.c build");
+    dev.check(.translate_c_command);
+
     const color: Color = .auto;
     assert(comp.c_source_files.len == 1);
     const c_source_file = comp.c_source_files[0];
@@ -4627,6 +4630,8 @@ const usage_init =
 ;
 
 fn cmdInit(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
+    dev.check(.init_command);
+
     {
         var i: usize = 0;
         while (i < args.len) : (i += 1) {
@@ -4678,6 +4683,8 @@ fn cmdInit(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
 }
 
 fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
+    dev.check(.build_command);
+
     var build_file: ?[]const u8 = null;
     var override_lib_dir: ?[]const u8 = try EnvVar.ZIG_LIB_DIR.get(arena);
     var override_global_cache_dir: ?[]const u8 = try EnvVar.ZIG_GLOBAL_CACHE_DIR.get(arena);
@@ -4969,16 +4976,12 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
     });
     defer thread_pool.deinit();
 
-    // Dummy http client that is not actually used when only_core_functionality is enabled.
+    // Dummy http client that is not actually used when fetch_command is unsupported.
     // Prevents bootstrap from depending on a bunch of unnecessary stuff.
-    const HttpClient = if (build_options.only_core_functionality) struct {
+    var http_client: if (dev.env.supports(.fetch_command)) std.http.Client else struct {
         allocator: Allocator,
-        fn deinit(self: *@This()) void {
-            _ = self;
-        }
-    } else std.http.Client;
-
-    var http_client: HttpClient = .{ .allocator = gpa };
+        fn deinit(_: @This()) void {}
+    } = .{ .allocator = gpa };
     defer http_client.deinit();
 
     var unlazy_set: Package.Fetch.JobQueue.UnlazySet = .{};
@@ -5045,16 +5048,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
             var cleanup_build_dir: ?fs.Dir = null;
             defer if (cleanup_build_dir) |*dir| dir.close();
 
-            if (build_options.only_core_functionality) {
-                try createEmptyDependenciesModule(
-                    arena,
-                    root_mod,
-                    global_cache_directory,
-                    local_cache_directory,
-                    builtin_mod,
-                    config,
-                );
-            } else {
+            if (dev.env.supports(.fetch_command)) {
                 const fetch_prog_node = root_prog_node.start("Fetch Packages", 0);
                 defer fetch_prog_node.end();
 
@@ -5203,7 +5197,14 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
                         }
                     }
                 }
-            }
+            } else try createEmptyDependenciesModule(
+                arena,
+                root_mod,
+                global_cache_directory,
+                local_cache_directory,
+                builtin_mod,
+                config,
+            );
 
             try root_mod.deps.put(arena, "@build", build_mod);
 
@@ -5269,7 +5270,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
                     if (code == 2) process.exit(2);
 
                     if (code == 3) {
-                        if (build_options.only_core_functionality) process.exit(3);
+                        if (!dev.env.supports(.fetch_command)) process.exit(3);
                         // Indicates the configure phase failed due to missing lazy
                         // dependencies and stdout contains the hashes of the ones
                         // that are missing.
@@ -5346,6 +5347,8 @@ fn jitCmd(
     args: []const []const u8,
     options: JitCmdOptions,
 ) !void {
+    dev.check(.jit_command);
+
     const color: Color = .auto;
     const root_prog_node = if (options.progress_node) |node| node else std.Progress.start(.{
         .disable_printing = (color == .off),
@@ -5995,6 +5998,8 @@ fn cmdAstCheck(
     arena: Allocator,
     args: []const []const u8,
 ) !void {
+    dev.check(.ast_check_command);
+
     const Zir = std.zig.Zir;
 
     var color: Color = .auto;
@@ -6154,6 +6159,8 @@ fn cmdDetectCpu(
     arena: Allocator,
     args: []const []const u8,
 ) !void {
+    dev.check(.detect_cpu_command);
+
     _ = gpa;
     _ = arena;
 
@@ -6293,6 +6300,8 @@ fn cmdDumpLlvmInts(
     arena: Allocator,
     args: []const []const u8,
 ) !void {
+    dev.check(.llvm_ints_command);
+
     _ = gpa;
 
     if (!build_options.have_llvm)
@@ -6336,6 +6345,8 @@ fn cmdDumpZir(
     arena: Allocator,
     args: []const []const u8,
 ) !void {
+    dev.check(.dump_zir_command);
+
     _ = arena;
     const Zir = std.zig.Zir;
 
@@ -6395,6 +6406,8 @@ fn cmdChangelist(
     arena: Allocator,
     args: []const []const u8,
 ) !void {
+    dev.check(.changelist_command);
+
     const color: Color = .auto;
     const Zir = std.zig.Zir;
 
@@ -6895,6 +6908,8 @@ fn cmdFetch(
     arena: Allocator,
     args: []const []const u8,
 ) !void {
+    dev.check(.fetch_command);
+
     const color: Color = .auto;
     const work_around_btrfs_bug = native_os == .linux and
         EnvVar.ZIG_BTRFS_WORKAROUND.isSet();
src/mingw.zig
@@ -9,6 +9,7 @@ const builtin = @import("builtin");
 const Compilation = @import("Compilation.zig");
 const build_options = @import("build_options");
 const Cache = std.Build.Cache;
+const dev = @import("dev.zig");
 
 pub const CRTFile = enum {
     crt2_o,
@@ -157,7 +158,8 @@ fn add_cc_args(
 }
 
 pub fn buildImportLib(comp: *Compilation, lib_name: []const u8) !void {
-    if (build_options.only_c) @compileError("building import libs not included in core functionality");
+    dev.check(.build_import_lib);
+
     var arena_allocator = std.heap.ArenaAllocator.init(comp.gpa);
     defer arena_allocator.deinit();
     const arena = arena_allocator.allocator();
src/Zcu.zig
@@ -38,6 +38,7 @@ const Alignment = InternPool.Alignment;
 const AnalUnit = InternPool.AnalUnit;
 const BuiltinFn = std.zig.BuiltinFn;
 const LlvmObject = @import("codegen/llvm.zig").Object;
+const dev = @import("dev.zig");
 
 comptime {
     @setEvalBranchQuota(4000);
@@ -57,7 +58,7 @@ comp: *Compilation,
 /// Usually, the LlvmObject is managed by linker code, however, in the case
 /// that -fno-emit-bin is specified, the linker code never executes, so we
 /// store the LlvmObject here.
-llvm_object: ?*LlvmObject,
+llvm_object: if (dev.env.supports(.llvm_backend)) ?*LlvmObject else ?noreturn,
 
 /// Pointer to externally managed resource.
 root_mod: *Package.Module,
@@ -2403,10 +2404,7 @@ pub fn deinit(zcu: *Zcu) void {
     const pt: Zcu.PerThread = .{ .tid = .main, .zcu = zcu };
     const gpa = zcu.gpa;
 
-    if (zcu.llvm_object) |llvm_object| {
-        if (build_options.only_c) unreachable;
-        llvm_object.deinit();
-    }
+    if (zcu.llvm_object) |llvm_object| llvm_object.deinit();
 
     for (zcu.import_table.keys()) |key| {
         gpa.free(key);
@@ -3041,7 +3039,7 @@ pub fn deleteUnitExports(zcu: *Zcu, anal_unit: AnalUnit) void {
     // `updateExports` on flush).
     // This case is needed because in some rare edge cases, `Sema` wants to add and delete exports
     // within a single update.
-    if (!build_options.only_c) {
+    if (dev.env.supports(.incremental)) {
         for (exports, exports_base..) |exp, export_idx| {
             if (zcu.comp.bin_file) |lf| {
                 lf.deleteExport(exp.exported, exp.opts.name);
stage1/config.zig.in
@@ -12,5 +12,4 @@ pub const enable_tracy = false;
 pub const value_tracing = false;
 pub const skip_non_native = false;
 pub const force_gpa = false;
-pub const only_c = false;
-pub const only_core_functionality = true;
+pub const dev = .core;
bootstrap.c
@@ -140,8 +140,7 @@ int main(int argc, char **argv) {
             "pub const value_tracing = false;\n"
             "pub const skip_non_native = false;\n"
             "pub const force_gpa = false;\n"
-            "pub const only_c = false;\n"
-            "pub const only_core_functionality = true;\n"
+            "pub const dev = .core;\n"
         , zig_version);
         if (written < 100)
             panic("unable to write to config.zig file");
build.zig
@@ -8,6 +8,7 @@ const io = std.io;
 const fs = std.fs;
 const InstallDirectoryOptions = std.Build.InstallDirectoryOptions;
 const assert = std.debug.assert;
+const DevEnv = @import("src/dev.zig").Env;
 
 const zig_version: std.SemanticVersion = .{ .major = 0, .minor = 14, .patch = 0 };
 const stack_size = 32 * 1024 * 1024;
@@ -232,8 +233,7 @@ pub fn build(b: *std.Build) !void {
     exe_options.addOption(bool, "llvm_has_arc", llvm_has_arc);
     exe_options.addOption(bool, "llvm_has_xtensa", llvm_has_xtensa);
     exe_options.addOption(bool, "force_gpa", force_gpa);
-    exe_options.addOption(bool, "only_c", only_c);
-    exe_options.addOption(bool, "only_core_functionality", only_c);
+    exe_options.addOption(DevEnv, "dev", b.option(DevEnv, "dev", "Build a compiler with a reduced feature set for development of specific features") orelse if (only_c) .bootstrap else .full);
 
     if (link_libc) {
         exe.linkLibC();
@@ -393,8 +393,6 @@ pub fn build(b: *std.Build) !void {
     test_cases_options.addOption(bool, "llvm_has_arc", llvm_has_arc);
     test_cases_options.addOption(bool, "llvm_has_xtensa", llvm_has_xtensa);
     test_cases_options.addOption(bool, "force_gpa", force_gpa);
-    test_cases_options.addOption(bool, "only_c", only_c);
-    test_cases_options.addOption(bool, "only_core_functionality", true);
     test_cases_options.addOption(bool, "enable_qemu", b.enable_qemu);
     test_cases_options.addOption(bool, "enable_wine", b.enable_wine);
     test_cases_options.addOption(bool, "enable_wasmtime", b.enable_wasmtime);
@@ -406,6 +404,7 @@ pub fn build(b: *std.Build) !void {
     test_cases_options.addOption([:0]const u8, "version", version);
     test_cases_options.addOption(std.SemanticVersion, "semver", semver);
     test_cases_options.addOption([]const []const u8, "test_filters", test_filters);
+    test_cases_options.addOption(DevEnv, "dev", if (only_c) .bootstrap else .core);
 
     var chosen_opt_modes_buf: [4]builtin.OptimizeMode = undefined;
     var chosen_mode_index: usize = 0;
@@ -575,7 +574,6 @@ fn addWasiUpdateStep(b: *std.Build, version: [:0]const u8) !void {
     exe_options.addOption(u32, "mem_leak_frames", 0);
     exe_options.addOption(bool, "have_llvm", false);
     exe_options.addOption(bool, "force_gpa", false);
-    exe_options.addOption(bool, "only_c", true);
     exe_options.addOption([:0]const u8, "version", version);
     exe_options.addOption(std.SemanticVersion, "semver", semver);
     exe_options.addOption(bool, "enable_debug_extensions", false);
@@ -585,7 +583,7 @@ fn addWasiUpdateStep(b: *std.Build, version: [:0]const u8) !void {
     exe_options.addOption(bool, "enable_tracy_callstack", false);
     exe_options.addOption(bool, "enable_tracy_allocation", false);
     exe_options.addOption(bool, "value_tracing", false);
-    exe_options.addOption(bool, "only_core_functionality", true);
+    exe_options.addOption(DevEnv, "dev", .bootstrap);
 
     const run_opt = b.addSystemCommand(&.{
         "wasm-opt",
CMakeLists.txt
@@ -578,6 +578,7 @@ set(ZIG_STAGE2_SOURCES
     src/codegen/spirv/Section.zig
     src/codegen/spirv/spec.zig
     src/crash_report.zig
+    src/dev.zig
     src/glibc.zig
     src/introspect.zig
     src/libcxx.zig