Commit 0bb93ca053

mlugg <mlugg@mlugg.co.uk>
2024-12-11 20:03:32
std.Build: simplify module dependency handling
At the expense of a slight special case in the build runner, we can make the handling of dependencies between modules a little shorter and much easier to follow. When module and step graphs are being constructed during the "configure" phase, we do not set up step dependencies triggered by modules. Instead, after the configure phase, the build runner traverses the whole step/module graph, starting from the root top-level steps, and configures all step dependencies implied by modules. The "make" phase then proceeds as normal. Also, the old `Module.dependencyIterator` logic is replaced by two separate iterables. `Module.getGraph` takes the root module of a compilation, and returns all modules in its graph; while `Step.Compile.getCompileDependencies` takes a `*Step.Compile` and returns all `*Step.Compile` it depends on, recursively, possibly excluding dynamic libraries. The old `Module.dependencyIterator` combined these two functions into one unintuitive iterator; they are now separated, which in particular helps readability at the usage sites which only need one or the other.
1 parent b83b161
Changed files (4)
lib/compiler/build_runner.zig
@@ -337,6 +337,7 @@ pub fn main() !void {
         var prog_node = main_progress_node.start("Configure", 0);
         defer prog_node.end();
         try builder.runBuild(root);
+        createModuleDependencies(builder) catch @panic("OOM");
     }
 
     if (graph.needed_lazy_dependencies.entries.len != 0) {
@@ -1440,3 +1441,80 @@ fn validateSystemLibraryOptions(b: *std.Build) void {
         process.exit(1);
     }
 }
+
+/// Starting from all top-level steps in `b`, traverses the entire step graph
+/// and adds all step dependencies implied by module graphs.
+fn createModuleDependencies(b: *std.Build) Allocator.Error!void {
+    const arena = b.graph.arena;
+
+    var all_steps: std.AutoArrayHashMapUnmanaged(*Step, void) = .empty;
+    var next_step_idx: usize = 0;
+
+    try all_steps.ensureUnusedCapacity(arena, b.top_level_steps.count());
+    for (b.top_level_steps.values()) |tls| {
+        all_steps.putAssumeCapacityNoClobber(&tls.step, {});
+    }
+
+    while (next_step_idx < all_steps.count()) {
+        const step = all_steps.keys()[next_step_idx];
+        next_step_idx += 1;
+
+        // Set up any implied dependencies for this step. It's important that we do this first, so
+        // that the loop below discovers steps implied by the module graph.
+        try createModuleDependenciesForStep(step);
+
+        try all_steps.ensureUnusedCapacity(arena, step.dependencies.items.len);
+        for (step.dependencies.items) |other_step| {
+            all_steps.putAssumeCapacity(other_step, {});
+        }
+    }
+}
+
+/// If the given `Step` is a `Step.Compile`, adds any dependencies for that step which
+/// are implied by the module graph rooted at `step.cast(Step.Compile).?.root_module`.
+fn createModuleDependenciesForStep(step: *Step) Allocator.Error!void {
+    const root_module = if (step.cast(Step.Compile)) |cs| root: {
+        break :root cs.root_module;
+    } else return; // not a compile step so no module dependencies
+
+    // Starting from `root_module`, discover all modules in this graph.
+    const modules = root_module.getGraph().modules;
+
+    // For each of those modules, set up the implied step dependencies.
+    for (modules) |mod| {
+        if (mod.root_source_file) |lp| lp.addStepDependencies(step);
+        for (mod.include_dirs.items) |include_dir| switch (include_dir) {
+            .path,
+            .path_system,
+            .path_after,
+            .framework_path,
+            .framework_path_system,
+            => |lp| lp.addStepDependencies(step),
+
+            .other_step => |other| {
+                other.getEmittedIncludeTree().addStepDependencies(step);
+                step.dependOn(&other.step);
+            },
+
+            .config_header_step => |other| step.dependOn(&other.step),
+        };
+        for (mod.lib_paths.items) |lp| lp.addStepDependencies(step);
+        for (mod.rpaths.items) |rpath| switch (rpath) {
+            .lazy_path => |lp| lp.addStepDependencies(step),
+            .special => {},
+        };
+        for (mod.link_objects.items) |link_object| switch (link_object) {
+            .static_path,
+            .assembly_file,
+            => |lp| lp.addStepDependencies(step),
+            .other_step => |other| step.dependOn(&other.step),
+            .system_lib => {},
+            .c_source_file => |source| source.file.addStepDependencies(step),
+            .c_source_files => |source_files| source_files.root.addStepDependencies(step),
+            .win32_resource_file => |rc_source| {
+                rc_source.file.addStepDependencies(step);
+                for (rc_source.include_paths) |lp| lp.addStepDependencies(step);
+            },
+        };
+    }
+}
lib/std/Build/Step/Compile.zig
@@ -389,7 +389,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile {
 
     const compile = owner.allocator.create(Compile) catch @panic("OOM");
     compile.* = .{
-        .root_module = undefined,
+        .root_module = options.root_module,
         .verbose_link = false,
         .verbose_cc = false,
         .linkage = options.linkage,
@@ -432,8 +432,6 @@ pub fn create(owner: *std.Build, options: Options) *Compile {
 
         .zig_process = null,
     };
-    options.root_module.init(owner, .{ .existing = options.root_module }, compile);
-    compile.root_module = options.root_module;
 
     if (options.zig_lib_dir) |lp| {
         compile.zig_lib_dir = lp.dupe(compile.step.owner);
@@ -607,16 +605,17 @@ pub fn dependsOnSystemLibrary(compile: *const Compile, name: []const u8) bool {
     var is_linking_libc = false;
     var is_linking_libcpp = false;
 
-    var dep_it = compile.root_module.iterateDependencies(compile, true);
-    while (dep_it.next()) |module| {
-        for (module.link_objects.items) |link_object| {
-            switch (link_object) {
-                .system_lib => |lib| if (mem.eql(u8, lib.name, name)) return true,
-                else => continue,
+    for (compile.getCompileDependencies(true)) |some_compile| {
+        for (some_compile.root_module.getGraph().modules) |mod| {
+            for (mod.link_objects.items) |lo| {
+                switch (lo) {
+                    .system_lib => |lib| if (mem.eql(u8, lib.name, name)) return true,
+                    else => {},
+                }
             }
+            if (mod.link_libc) is_linking_libc = true;
+            if (mod.link_libcpp) is_linking_libcpp = true;
         }
-        is_linking_libc = is_linking_libc or module.link_libc == true;
-        is_linking_libcpp = is_linking_libcpp or module.link_libcpp == true;
     }
 
     const target = compile.rootModuleTarget();
@@ -961,23 +960,22 @@ const CliNamedModules = struct {
             .modules = .{},
             .names = .{},
         };
-        var dep_it = root_module.iterateDependencies(null, false);
+        const graph = root_module.getGraph();
         {
-            const item = dep_it.next().?;
-            assert(root_module == item.module);
+            assert(graph.modules[0] == root_module);
             try compile.modules.put(arena, root_module, {});
             try compile.names.put(arena, "root", {});
         }
-        while (dep_it.next()) |item| {
-            var name = item.name;
+        for (graph.modules[1..], graph.names[1..]) |mod, orig_name| {
+            var name = orig_name;
             var n: usize = 0;
             while (true) {
                 const gop = try compile.names.getOrPut(arena, name);
                 if (!gop.found_existing) {
-                    try compile.modules.putNoClobber(arena, item.module, {});
+                    try compile.modules.putNoClobber(arena, mod, {});
                     break;
                 }
-                name = try std.fmt.allocPrint(arena, "{s}{d}", .{ item.name, n });
+                name = try std.fmt.allocPrint(arena, "{s}{d}", .{ orig_name, n });
                 n += 1;
             }
         }
@@ -1080,13 +1078,12 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 {
         // emitted if there is nothing to link.
         var total_linker_objects: usize = @intFromBool(compile.root_module.root_source_file != null);
 
-        {
-            // Fully recursive iteration including dynamic libraries to detect
-            // libc and libc++ linkage.
-            var dep_it = compile.root_module.iterateDependencies(compile, true);
-            while (dep_it.next()) |key| {
-                if (key.module.link_libc == true) compile.is_linking_libc = true;
-                if (key.module.link_libcpp == true) compile.is_linking_libcpp = true;
+        // Fully recursive iteration including dynamic libraries to detect
+        // libc and libc++ linkage.
+        for (compile.getCompileDependencies(true)) |some_compile| {
+            for (some_compile.root_module.getGraph().modules) |mod| {
+                if (mod.link_libc == true) compile.is_linking_libc = true;
+                if (mod.link_libcpp == true) compile.is_linking_libcpp = true;
             }
         }
 
@@ -1094,265 +1091,265 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 {
 
         // For this loop, don't chase dynamic libraries because their link
         // objects are already linked.
-        var dep_it = compile.root_module.iterateDependencies(compile, false);
-
-        while (dep_it.next()) |dep| {
-            // While walking transitive dependencies, if a given link object is
-            // already included in a library, it should not redundantly be
-            // placed on the linker line of the dependee.
-            const my_responsibility = dep.compile.? == compile;
-            const already_linked = !my_responsibility and dep.compile.?.isDynamicLibrary();
-
-            // Inherit dependencies on darwin frameworks.
-            if (!already_linked) {
-                for (dep.module.frameworks.keys(), dep.module.frameworks.values()) |name, info| {
-                    try frameworks.put(arena, name, info);
+        for (compile.getCompileDependencies(false)) |dep_compile| {
+            for (dep_compile.root_module.getGraph().modules) |mod| {
+                // While walking transitive dependencies, if a given link object is
+                // already included in a library, it should not redundantly be
+                // placed on the linker line of the dependee.
+                const my_responsibility = dep_compile == compile;
+                const already_linked = !my_responsibility and dep_compile.isDynamicLibrary();
+
+                // Inherit dependencies on darwin frameworks.
+                if (!already_linked) {
+                    for (mod.frameworks.keys(), mod.frameworks.values()) |name, info| {
+                        try frameworks.put(arena, name, info);
+                    }
                 }
-            }
-
-            // Inherit dependencies on system libraries and static libraries.
-            for (dep.module.link_objects.items) |link_object| {
-                switch (link_object) {
-                    .static_path => |static_path| {
-                        if (my_responsibility) {
-                            try zig_args.append(static_path.getPath2(dep.module.owner, step));
-                            total_linker_objects += 1;
-                        }
-                    },
-                    .system_lib => |system_lib| {
-                        const system_lib_gop = try seen_system_libs.getOrPut(arena, system_lib.name);
-                        if (system_lib_gop.found_existing) {
-                            try zig_args.appendSlice(system_lib_gop.value_ptr.*);
-                            continue;
-                        } else {
-                            system_lib_gop.value_ptr.* = &.{};
-                        }
 
-                        if (already_linked)
-                            continue;
-
-                        if ((system_lib.search_strategy != prev_search_strategy or
-                            system_lib.preferred_link_mode != prev_preferred_link_mode) and
-                            compile.linkage != .static)
-                        {
-                            switch (system_lib.search_strategy) {
-                                .no_fallback => switch (system_lib.preferred_link_mode) {
-                                    .dynamic => try zig_args.append("-search_dylibs_only"),
-                                    .static => try zig_args.append("-search_static_only"),
-                                },
-                                .paths_first => switch (system_lib.preferred_link_mode) {
-                                    .dynamic => try zig_args.append("-search_paths_first"),
-                                    .static => try zig_args.append("-search_paths_first_static"),
-                                },
-                                .mode_first => switch (system_lib.preferred_link_mode) {
-                                    .dynamic => try zig_args.append("-search_dylibs_first"),
-                                    .static => try zig_args.append("-search_static_first"),
-                                },
+                // Inherit dependencies on system libraries and static libraries.
+                for (mod.link_objects.items) |link_object| {
+                    switch (link_object) {
+                        .static_path => |static_path| {
+                            if (my_responsibility) {
+                                try zig_args.append(static_path.getPath2(mod.owner, step));
+                                total_linker_objects += 1;
+                            }
+                        },
+                        .system_lib => |system_lib| {
+                            const system_lib_gop = try seen_system_libs.getOrPut(arena, system_lib.name);
+                            if (system_lib_gop.found_existing) {
+                                try zig_args.appendSlice(system_lib_gop.value_ptr.*);
+                                continue;
+                            } else {
+                                system_lib_gop.value_ptr.* = &.{};
                             }
-                            prev_search_strategy = system_lib.search_strategy;
-                            prev_preferred_link_mode = system_lib.preferred_link_mode;
-                        }
 
-                        const prefix: []const u8 = prefix: {
-                            if (system_lib.needed) break :prefix "-needed-l";
-                            if (system_lib.weak) break :prefix "-weak-l";
-                            break :prefix "-l";
-                        };
-                        switch (system_lib.use_pkg_config) {
-                            .no => try zig_args.append(b.fmt("{s}{s}", .{ prefix, system_lib.name })),
-                            .yes, .force => {
-                                if (compile.runPkgConfig(system_lib.name)) |result| {
-                                    try zig_args.appendSlice(result.cflags);
-                                    try zig_args.appendSlice(result.libs);
-                                    try seen_system_libs.put(arena, system_lib.name, result.cflags);
-                                } else |err| switch (err) {
-                                    error.PkgConfigInvalidOutput,
-                                    error.PkgConfigCrashed,
-                                    error.PkgConfigFailed,
-                                    error.PkgConfigNotInstalled,
-                                    error.PackageNotFound,
-                                    => switch (system_lib.use_pkg_config) {
-                                        .yes => {
-                                            // pkg-config failed, so fall back to linking the library
-                                            // by name directly.
-                                            try zig_args.append(b.fmt("{s}{s}", .{
-                                                prefix,
-                                                system_lib.name,
-                                            }));
-                                        },
-                                        .force => {
-                                            panic("pkg-config failed for library {s}", .{system_lib.name});
-                                        },
-                                        .no => unreachable,
+                            if (already_linked)
+                                continue;
+
+                            if ((system_lib.search_strategy != prev_search_strategy or
+                                system_lib.preferred_link_mode != prev_preferred_link_mode) and
+                                compile.linkage != .static)
+                            {
+                                switch (system_lib.search_strategy) {
+                                    .no_fallback => switch (system_lib.preferred_link_mode) {
+                                        .dynamic => try zig_args.append("-search_dylibs_only"),
+                                        .static => try zig_args.append("-search_static_only"),
+                                    },
+                                    .paths_first => switch (system_lib.preferred_link_mode) {
+                                        .dynamic => try zig_args.append("-search_paths_first"),
+                                        .static => try zig_args.append("-search_paths_first_static"),
+                                    },
+                                    .mode_first => switch (system_lib.preferred_link_mode) {
+                                        .dynamic => try zig_args.append("-search_dylibs_first"),
+                                        .static => try zig_args.append("-search_static_first"),
                                     },
-
-                                    else => |e| return e,
-                                }
-                            },
-                        }
-                    },
-                    .other_step => |other| {
-                        switch (other.kind) {
-                            .exe => return step.fail("cannot link with an executable build artifact", .{}),
-                            .@"test" => return step.fail("cannot link with a test", .{}),
-                            .obj => {
-                                const included_in_lib_or_obj = !my_responsibility and
-                                    (dep.compile.?.kind == .lib or dep.compile.?.kind == .obj);
-                                if (!already_linked and !included_in_lib_or_obj) {
-                                    try zig_args.append(other.getEmittedBin().getPath2(b, step));
-                                    total_linker_objects += 1;
-                                }
-                            },
-                            .lib => l: {
-                                const other_produces_implib = other.producesImplib();
-                                const other_is_static = other_produces_implib or other.isStaticLibrary();
-
-                                if (compile.isStaticLibrary() and other_is_static) {
-                                    // Avoid putting a static library inside a static library.
-                                    break :l;
                                 }
+                                prev_search_strategy = system_lib.search_strategy;
+                                prev_preferred_link_mode = system_lib.preferred_link_mode;
+                            }
 
-                                // For DLLs, we must link against the implib.
-                                // For everything else, we directly link
-                                // against the library file.
-                                const full_path_lib = if (other_produces_implib)
-                                    other.getGeneratedFilePath("generated_implib", &compile.step)
-                                else
-                                    other.getGeneratedFilePath("generated_bin", &compile.step);
+                            const prefix: []const u8 = prefix: {
+                                if (system_lib.needed) break :prefix "-needed-l";
+                                if (system_lib.weak) break :prefix "-weak-l";
+                                break :prefix "-l";
+                            };
+                            switch (system_lib.use_pkg_config) {
+                                .no => try zig_args.append(b.fmt("{s}{s}", .{ prefix, system_lib.name })),
+                                .yes, .force => {
+                                    if (compile.runPkgConfig(system_lib.name)) |result| {
+                                        try zig_args.appendSlice(result.cflags);
+                                        try zig_args.appendSlice(result.libs);
+                                        try seen_system_libs.put(arena, system_lib.name, result.cflags);
+                                    } else |err| switch (err) {
+                                        error.PkgConfigInvalidOutput,
+                                        error.PkgConfigCrashed,
+                                        error.PkgConfigFailed,
+                                        error.PkgConfigNotInstalled,
+                                        error.PackageNotFound,
+                                        => switch (system_lib.use_pkg_config) {
+                                            .yes => {
+                                                // pkg-config failed, so fall back to linking the library
+                                                // by name directly.
+                                                try zig_args.append(b.fmt("{s}{s}", .{
+                                                    prefix,
+                                                    system_lib.name,
+                                                }));
+                                            },
+                                            .force => {
+                                                panic("pkg-config failed for library {s}", .{system_lib.name});
+                                            },
+                                            .no => unreachable,
+                                        },
 
-                                try zig_args.append(full_path_lib);
-                                total_linker_objects += 1;
+                                        else => |e| return e,
+                                    }
+                                },
+                            }
+                        },
+                        .other_step => |other| {
+                            switch (other.kind) {
+                                .exe => return step.fail("cannot link with an executable build artifact", .{}),
+                                .@"test" => return step.fail("cannot link with a test", .{}),
+                                .obj => {
+                                    const included_in_lib_or_obj = !my_responsibility and
+                                        (dep_compile.kind == .lib or dep_compile.kind == .obj);
+                                    if (!already_linked and !included_in_lib_or_obj) {
+                                        try zig_args.append(other.getEmittedBin().getPath2(b, step));
+                                        total_linker_objects += 1;
+                                    }
+                                },
+                                .lib => l: {
+                                    const other_produces_implib = other.producesImplib();
+                                    const other_is_static = other_produces_implib or other.isStaticLibrary();
 
-                                if (other.linkage == .dynamic and
-                                    compile.rootModuleTarget().os.tag != .windows)
-                                {
-                                    if (fs.path.dirname(full_path_lib)) |dirname| {
-                                        try zig_args.append("-rpath");
-                                        try zig_args.append(dirname);
+                                    if (compile.isStaticLibrary() and other_is_static) {
+                                        // Avoid putting a static library inside a static library.
+                                        break :l;
                                     }
-                                }
-                            },
-                        }
-                    },
-                    .assembly_file => |asm_file| l: {
-                        if (!my_responsibility) break :l;
 
-                        if (prev_has_cflags) {
-                            try zig_args.append("-cflags");
-                            try zig_args.append("--");
-                            prev_has_cflags = false;
-                        }
-                        try zig_args.append(asm_file.getPath2(dep.module.owner, step));
-                        total_linker_objects += 1;
-                    },
+                                    // For DLLs, we must link against the implib.
+                                    // For everything else, we directly link
+                                    // against the library file.
+                                    const full_path_lib = if (other_produces_implib)
+                                        other.getGeneratedFilePath("generated_implib", &compile.step)
+                                    else
+                                        other.getGeneratedFilePath("generated_bin", &compile.step);
 
-                    .c_source_file => |c_source_file| l: {
-                        if (!my_responsibility) break :l;
+                                    try zig_args.append(full_path_lib);
+                                    total_linker_objects += 1;
+
+                                    if (other.linkage == .dynamic and
+                                        compile.rootModuleTarget().os.tag != .windows)
+                                    {
+                                        if (fs.path.dirname(full_path_lib)) |dirname| {
+                                            try zig_args.append("-rpath");
+                                            try zig_args.append(dirname);
+                                        }
+                                    }
+                                },
+                            }
+                        },
+                        .assembly_file => |asm_file| l: {
+                            if (!my_responsibility) break :l;
 
-                        if (c_source_file.flags.len == 0) {
                             if (prev_has_cflags) {
                                 try zig_args.append("-cflags");
                                 try zig_args.append("--");
                                 prev_has_cflags = false;
                             }
-                        } else {
-                            try zig_args.append("-cflags");
-                            for (c_source_file.flags) |arg| {
-                                try zig_args.append(arg);
-                            }
-                            try zig_args.append("--");
-                            prev_has_cflags = true;
-                        }
-                        try zig_args.append(c_source_file.file.getPath2(dep.module.owner, step));
-                        total_linker_objects += 1;
-                    },
+                            try zig_args.append(asm_file.getPath2(mod.owner, step));
+                            total_linker_objects += 1;
+                        },
 
-                    .c_source_files => |c_source_files| l: {
-                        if (!my_responsibility) break :l;
+                        .c_source_file => |c_source_file| l: {
+                            if (!my_responsibility) break :l;
 
-                        if (c_source_files.flags.len == 0) {
-                            if (prev_has_cflags) {
+                            if (c_source_file.flags.len == 0) {
+                                if (prev_has_cflags) {
+                                    try zig_args.append("-cflags");
+                                    try zig_args.append("--");
+                                    prev_has_cflags = false;
+                                }
+                            } else {
                                 try zig_args.append("-cflags");
+                                for (c_source_file.flags) |arg| {
+                                    try zig_args.append(arg);
+                                }
                                 try zig_args.append("--");
-                                prev_has_cflags = false;
+                                prev_has_cflags = true;
                             }
-                        } else {
-                            try zig_args.append("-cflags");
-                            for (c_source_files.flags) |flag| {
-                                try zig_args.append(flag);
+                            try zig_args.append(c_source_file.file.getPath2(mod.owner, step));
+                            total_linker_objects += 1;
+                        },
+
+                        .c_source_files => |c_source_files| l: {
+                            if (!my_responsibility) break :l;
+
+                            if (c_source_files.flags.len == 0) {
+                                if (prev_has_cflags) {
+                                    try zig_args.append("-cflags");
+                                    try zig_args.append("--");
+                                    prev_has_cflags = false;
+                                }
+                            } else {
+                                try zig_args.append("-cflags");
+                                for (c_source_files.flags) |flag| {
+                                    try zig_args.append(flag);
+                                }
+                                try zig_args.append("--");
+                                prev_has_cflags = true;
                             }
-                            try zig_args.append("--");
-                            prev_has_cflags = true;
-                        }
 
-                        const root_path = c_source_files.root.getPath2(dep.module.owner, step);
-                        for (c_source_files.files) |file| {
-                            try zig_args.append(b.pathJoin(&.{ root_path, file }));
-                        }
+                            const root_path = c_source_files.root.getPath2(mod.owner, step);
+                            for (c_source_files.files) |file| {
+                                try zig_args.append(b.pathJoin(&.{ root_path, file }));
+                            }
 
-                        total_linker_objects += c_source_files.files.len;
-                    },
+                            total_linker_objects += c_source_files.files.len;
+                        },
 
-                    .win32_resource_file => |rc_source_file| l: {
-                        if (!my_responsibility) break :l;
+                        .win32_resource_file => |rc_source_file| l: {
+                            if (!my_responsibility) break :l;
 
-                        if (rc_source_file.flags.len == 0 and rc_source_file.include_paths.len == 0) {
-                            if (prev_has_rcflags) {
+                            if (rc_source_file.flags.len == 0 and rc_source_file.include_paths.len == 0) {
+                                if (prev_has_rcflags) {
+                                    try zig_args.append("-rcflags");
+                                    try zig_args.append("--");
+                                    prev_has_rcflags = false;
+                                }
+                            } else {
                                 try zig_args.append("-rcflags");
+                                for (rc_source_file.flags) |arg| {
+                                    try zig_args.append(arg);
+                                }
+                                for (rc_source_file.include_paths) |include_path| {
+                                    try zig_args.append("/I");
+                                    try zig_args.append(include_path.getPath2(mod.owner, step));
+                                }
                                 try zig_args.append("--");
-                                prev_has_rcflags = false;
-                            }
-                        } else {
-                            try zig_args.append("-rcflags");
-                            for (rc_source_file.flags) |arg| {
-                                try zig_args.append(arg);
-                            }
-                            for (rc_source_file.include_paths) |include_path| {
-                                try zig_args.append("/I");
-                                try zig_args.append(include_path.getPath2(dep.module.owner, step));
+                                prev_has_rcflags = true;
                             }
-                            try zig_args.append("--");
-                            prev_has_rcflags = true;
-                        }
-                        try zig_args.append(rc_source_file.file.getPath2(dep.module.owner, step));
-                        total_linker_objects += 1;
-                    },
+                            try zig_args.append(rc_source_file.file.getPath2(mod.owner, step));
+                            total_linker_objects += 1;
+                        },
+                    }
                 }
-            }
 
-            // We need to emit the --mod argument here so that the above link objects
-            // have the correct parent module, but only if the module is part of
-            // this compilation.
-            if (!my_responsibility) continue;
-            if (cli_named_modules.modules.getIndex(dep.module)) |module_cli_index| {
-                const module_cli_name = cli_named_modules.names.keys()[module_cli_index];
-                try dep.module.appendZigProcessFlags(&zig_args, step);
-
-                // --dep arguments
-                try zig_args.ensureUnusedCapacity(dep.module.import_table.count() * 2);
-                for (dep.module.import_table.keys(), dep.module.import_table.values()) |name, import| {
-                    const import_index = cli_named_modules.modules.getIndex(import).?;
-                    const import_cli_name = cli_named_modules.names.keys()[import_index];
-                    zig_args.appendAssumeCapacity("--dep");
-                    if (std.mem.eql(u8, import_cli_name, name)) {
-                        zig_args.appendAssumeCapacity(import_cli_name);
-                    } else {
-                        zig_args.appendAssumeCapacity(b.fmt("{s}={s}", .{ name, import_cli_name }));
+                // We need to emit the --mod argument here so that the above link objects
+                // have the correct parent module, but only if the module is part of
+                // this compilation.
+                if (!my_responsibility) continue;
+                if (cli_named_modules.modules.getIndex(mod)) |module_cli_index| {
+                    const module_cli_name = cli_named_modules.names.keys()[module_cli_index];
+                    try mod.appendZigProcessFlags(&zig_args, step);
+
+                    // --dep arguments
+                    try zig_args.ensureUnusedCapacity(mod.import_table.count() * 2);
+                    for (mod.import_table.keys(), mod.import_table.values()) |name, import| {
+                        const import_index = cli_named_modules.modules.getIndex(import).?;
+                        const import_cli_name = cli_named_modules.names.keys()[import_index];
+                        zig_args.appendAssumeCapacity("--dep");
+                        if (std.mem.eql(u8, import_cli_name, name)) {
+                            zig_args.appendAssumeCapacity(import_cli_name);
+                        } else {
+                            zig_args.appendAssumeCapacity(b.fmt("{s}={s}", .{ name, import_cli_name }));
+                        }
                     }
-                }
 
-                // When the CLI sees a -M argument, it determines whether it
-                // implies the existence of a Zig compilation unit based on
-                // whether there is a root source file. If there is no root
-                // source file, then this is not a zig compilation unit - it is
-                // perhaps a set of linker objects, or C source files instead.
-                // Linker objects are added to the CLI globally, while C source
-                // files must have a module parent.
-                if (dep.module.root_source_file) |lp| {
-                    const src = lp.getPath2(dep.module.owner, step);
-                    try zig_args.append(b.fmt("-M{s}={s}", .{ module_cli_name, src }));
-                } else if (moduleNeedsCliArg(dep.module)) {
-                    try zig_args.append(b.fmt("-M{s}", .{module_cli_name}));
+                    // When the CLI sees a -M argument, it determines whether it
+                    // implies the existence of a Zig compilation unit based on
+                    // whether there is a root source file. If there is no root
+                    // source file, then this is not a zig compilation unit - it is
+                    // perhaps a set of linker objects, or C source files instead.
+                    // Linker objects are added to the CLI globally, while C source
+                    // files must have a module parent.
+                    if (mod.root_source_file) |lp| {
+                        const src = lp.getPath2(mod.owner, step);
+                        try zig_args.append(b.fmt("-M{s}={s}", .{ module_cli_name, src }));
+                    } else if (moduleNeedsCliArg(mod)) {
+                        try zig_args.append(b.fmt("-M{s}", .{module_cli_name}));
+                    }
                 }
             }
         }
@@ -2064,3 +2061,35 @@ fn moduleNeedsCliArg(mod: *const Module) bool {
         else => continue,
     } else false;
 }
+
+/// Return the full set of `Step.Compile` which `start` depends on, recursively. `start` itself is
+/// always returned as the first element. If `chase_dynamic` is `false`, then dynamic libraries are
+/// not included, and their dependencies are not considered; if `chase_dynamic` is `true`, dynamic
+/// libraries are treated the same as other linked `Compile`s.
+pub fn getCompileDependencies(start: *Compile, chase_dynamic: bool) []const *Compile {
+    const arena = start.step.owner.graph.arena;
+
+    var compiles: std.AutoArrayHashMapUnmanaged(*Compile, void) = .empty;
+    var next_idx: usize = 0;
+
+    compiles.putNoClobber(arena, start, {}) catch @panic("OOM");
+
+    while (next_idx < compiles.count()) {
+        const compile = compiles.keys()[next_idx];
+        next_idx += 1;
+
+        for (compile.root_module.getGraph().modules) |mod| {
+            for (mod.link_objects.items) |lo| {
+                switch (lo) {
+                    .other_step => |other_compile| {
+                        if (!chase_dynamic and other_compile.isDynamicLibrary()) continue;
+                        compiles.put(arena, other_compile, {}) catch @panic("OOM");
+                    },
+                    else => {},
+                }
+            }
+        }
+    }
+
+    return compiles.keys();
+}
lib/std/Build/Step/Run.zig
@@ -1719,15 +1719,12 @@ fn evalGeneric(run: *Run, child: *std.process.Child) !StdIoResult {
 
 fn addPathForDynLibs(run: *Run, artifact: *Step.Compile) void {
     const b = run.step.owner;
-    var it = artifact.root_module.iterateDependencies(artifact, true);
-    while (it.next()) |item| {
-        const other = item.compile.?;
-        if (item.module == other.root_module) {
-            if (item.module.resolved_target.?.result.os.tag == .windows and
-                other.isDynamicLibrary())
-            {
-                addPathDir(run, fs.path.dirname(other.getEmittedBin().getPath2(b, &run.step)).?);
-            }
+    const compiles = artifact.getCompileDependencies(true);
+    for (compiles) |compile| {
+        if (compile.root_module.resolved_target.?.result.os.tag == .windows and
+            compile.isDynamicLibrary())
+        {
+            addPathDir(run, fs.path.dirname(compile.getEmittedBin().getPath2(b, &run.step)).?);
         }
     }
 }
lib/std/Build/Module.zig
@@ -1,9 +1,5 @@
 /// The one responsible for creating this module.
 owner: *std.Build,
-/// Tracks the set of steps that depend on this `Module`. This ensures that
-/// when making this `Module` depend on other `Module` objects and `Step`
-/// objects, respective `Step` dependencies can be added.
-depending_steps: std.AutoArrayHashMapUnmanaged(*Step.Compile, void),
 root_source_file: ?LazyPath,
 /// The modules that are mapped into this module's import table.
 /// Use `addImport` rather than modifying this field directly in order to
@@ -41,6 +37,10 @@ link_libcpp: ?bool,
 /// Symbols to be exported when compiling to WebAssembly.
 export_symbol_names: []const []const u8 = &.{},
 
+/// Caches the result of `getGraph` when called multiple times.
+/// Use `getGraph` instead of accessing this field directly.
+cached_graph: Graph = .{ .modules = &.{}, .names = &.{} },
+
 pub const RPath = union(enum) {
     lazy_path: LazyPath,
     special: []const u8,
@@ -246,7 +246,6 @@ pub fn init(
     m: *Module,
     owner: *std.Build,
     value: union(enum) { options: CreateOptions, existing: *const Module },
-    compile: ?*Step.Compile,
 ) void {
     const allocator = owner.allocator;
 
@@ -254,7 +253,6 @@ pub fn init(
         .options => |options| {
             m.* = .{
                 .owner = owner,
-                .depending_steps = .{},
                 .root_source_file = if (options.root_source_file) |lp| lp.dupe(owner) else null,
                 .import_table = .{},
                 .resolved_target = options.target,
@@ -294,19 +292,11 @@ pub fn init(
             m.* = existing.*;
         },
     }
-
-    if (compile) |c| {
-        m.depending_steps.put(allocator, c, {}) catch @panic("OOM");
-    }
-
-    // This logic accesses `depending_steps` which was just modified above.
-    var it = m.iterateDependencies(null, false);
-    while (it.next()) |item| addShallowDependencies(m, item.module);
 }
 
 pub fn create(owner: *std.Build, options: CreateOptions) *Module {
     const m = owner.allocator.create(Module) catch @panic("OOM");
-    m.init(owner, .{ .options = options }, null);
+    m.init(owner, .{ .options = options });
     return m;
 }
 
@@ -314,69 +304,6 @@ pub fn create(owner: *std.Build, options: CreateOptions) *Module {
 pub fn addImport(m: *Module, name: []const u8, module: *Module) void {
     const b = m.owner;
     m.import_table.put(b.allocator, b.dupe(name), module) catch @panic("OOM");
-
-    var it = module.iterateDependencies(null, false);
-    while (it.next()) |item| addShallowDependencies(m, item.module);
-}
-
-/// Creates step dependencies and updates `depending_steps` of `dependee` so that
-/// subsequent calls to `addImport` on `dependee` will additionally create step
-/// dependencies on `m`'s `depending_steps`.
-fn addShallowDependencies(m: *Module, dependee: *Module) void {
-    if (dependee.root_source_file) |lazy_path| addLazyPathDependencies(m, dependee, lazy_path);
-    for (dependee.lib_paths.items) |lib_path| addLazyPathDependencies(m, dependee, lib_path);
-    for (dependee.rpaths.items) |rpath| switch (rpath) {
-        .lazy_path => |lp| addLazyPathDependencies(m, dependee, lp),
-        .special => {},
-    };
-
-    for (dependee.link_objects.items) |link_object| switch (link_object) {
-        .other_step => |compile| {
-            addStepDependencies(m, dependee, &compile.step);
-            addLazyPathDependenciesOnly(m, compile.getEmittedIncludeTree());
-        },
-
-        .static_path,
-        .assembly_file,
-        => |lp| addLazyPathDependencies(m, dependee, lp),
-
-        .c_source_file => |x| addLazyPathDependencies(m, dependee, x.file),
-        .win32_resource_file => |x| addLazyPathDependencies(m, dependee, x.file),
-
-        .c_source_files,
-        .system_lib,
-        => {},
-    };
-}
-
-fn addLazyPathDependencies(m: *Module, module: *Module, lazy_path: LazyPath) void {
-    addLazyPathDependenciesOnly(m, lazy_path);
-    if (m != module) {
-        for (m.depending_steps.keys()) |compile| {
-            module.depending_steps.put(m.owner.allocator, compile, {}) catch @panic("OOM");
-        }
-    }
-}
-
-fn addLazyPathDependenciesOnly(m: *Module, lazy_path: LazyPath) void {
-    for (m.depending_steps.keys()) |compile| {
-        lazy_path.addStepDependencies(&compile.step);
-    }
-}
-
-fn addStepDependencies(m: *Module, module: *Module, dependee: *Step) void {
-    addStepDependenciesOnly(m, dependee);
-    if (m != module) {
-        for (m.depending_steps.keys()) |compile| {
-            module.depending_steps.put(m.owner.allocator, compile, {}) catch @panic("OOM");
-        }
-    }
-}
-
-fn addStepDependenciesOnly(m: *Module, dependee: *Step) void {
-    for (m.depending_steps.keys()) |compile| {
-        compile.step.dependOn(dependee);
-    }
 }
 
 /// Creates a new module and adds it to be used with `@import`.
@@ -392,91 +319,6 @@ pub fn addOptions(m: *Module, module_name: []const u8, options: *Step.Options) v
     addImport(m, module_name, options.createModule());
 }
 
-pub const DependencyIterator = struct {
-    allocator: std.mem.Allocator,
-    index: usize,
-    set: std.AutoArrayHashMapUnmanaged(Key, []const u8),
-    chase_dyn_libs: bool,
-
-    pub const Key = struct {
-        /// The compilation that contains the `Module`. Note that a `Module` might be
-        /// used by more than one compilation.
-        compile: ?*Step.Compile,
-        module: *Module,
-    };
-
-    pub const Item = struct {
-        /// The compilation that contains the `Module`. Note that a `Module` might be
-        /// used by more than one compilation.
-        compile: ?*Step.Compile,
-        module: *Module,
-        name: []const u8,
-    };
-
-    pub fn deinit(it: *DependencyIterator) void {
-        it.set.deinit(it.allocator);
-        it.* = undefined;
-    }
-
-    pub fn next(it: *DependencyIterator) ?Item {
-        if (it.index >= it.set.count()) {
-            it.set.clearAndFree(it.allocator);
-            return null;
-        }
-        const key = it.set.keys()[it.index];
-        const name = it.set.values()[it.index];
-        it.index += 1;
-        const module = key.module;
-        it.set.ensureUnusedCapacity(it.allocator, module.import_table.count()) catch
-            @panic("OOM");
-        for (module.import_table.keys(), module.import_table.values()) |dep_name, dep| {
-            it.set.putAssumeCapacity(.{
-                .module = dep,
-                .compile = key.compile,
-            }, dep_name);
-        }
-
-        if (key.compile != null) {
-            for (module.link_objects.items) |link_object| switch (link_object) {
-                .other_step => |compile| {
-                    if (!it.chase_dyn_libs and compile.isDynamicLibrary()) continue;
-
-                    it.set.put(it.allocator, .{
-                        .module = compile.root_module,
-                        .compile = compile,
-                    }, "root") catch @panic("OOM");
-                },
-                else => {},
-            };
-        }
-
-        return .{
-            .compile = key.compile,
-            .module = key.module,
-            .name = name,
-        };
-    }
-};
-
-pub fn iterateDependencies(
-    m: *Module,
-    chase_steps: ?*Step.Compile,
-    chase_dyn_libs: bool,
-) DependencyIterator {
-    var it: DependencyIterator = .{
-        .allocator = m.owner.allocator,
-        .index = 0,
-        .set = .{},
-        .chase_dyn_libs = chase_dyn_libs,
-    };
-    it.set.ensureUnusedCapacity(m.owner.allocator, m.import_table.count() + 1) catch @panic("OOM");
-    it.set.putAssumeCapacity(.{
-        .module = m,
-        .compile = chase_steps,
-    }, "root");
-    return it;
-}
-
 pub const LinkSystemLibraryOptions = struct {
     /// Causes dynamic libraries to be linked regardless of whether they are
     /// actually depended on. When false, dynamic libraries with no referenced
@@ -559,7 +401,6 @@ pub fn addCSourceFiles(m: *Module, options: AddCSourceFilesOptions) void {
         .flags = b.dupeStrings(options.flags),
     };
     m.link_objects.append(allocator, .{ .c_source_files = c_source_files }) catch @panic("OOM");
-    addLazyPathDependenciesOnly(m, c_source_files.root);
 }
 
 pub fn addCSourceFile(m: *Module, source: CSourceFile) void {
@@ -568,7 +409,6 @@ pub fn addCSourceFile(m: *Module, source: CSourceFile) void {
     const c_source_file = allocator.create(CSourceFile) catch @panic("OOM");
     c_source_file.* = source.dupe(b);
     m.link_objects.append(allocator, .{ .c_source_file = c_source_file }) catch @panic("OOM");
-    addLazyPathDependenciesOnly(m, source.file);
 }
 
 /// Resource files must have the extension `.rc`.
@@ -585,22 +425,16 @@ pub fn addWin32ResourceFile(m: *Module, source: RcSourceFile) void {
     const rc_source_file = allocator.create(RcSourceFile) catch @panic("OOM");
     rc_source_file.* = source.dupe(b);
     m.link_objects.append(allocator, .{ .win32_resource_file = rc_source_file }) catch @panic("OOM");
-    addLazyPathDependenciesOnly(m, source.file);
-    for (source.include_paths) |include_path| {
-        addLazyPathDependenciesOnly(m, include_path);
-    }
 }
 
 pub fn addAssemblyFile(m: *Module, source: LazyPath) void {
     const b = m.owner;
     m.link_objects.append(b.allocator, .{ .assembly_file = source.dupe(b) }) catch @panic("OOM");
-    addLazyPathDependenciesOnly(m, source);
 }
 
 pub fn addObjectFile(m: *Module, object: LazyPath) void {
     const b = m.owner;
     m.link_objects.append(b.allocator, .{ .static_path = object.dupe(b) }) catch @panic("OOM");
-    addLazyPathDependenciesOnly(m, object);
 }
 
 pub fn addObject(m: *Module, object: *Step.Compile) void {
@@ -616,51 +450,43 @@ pub fn linkLibrary(m: *Module, library: *Step.Compile) void {
 pub fn addAfterIncludePath(m: *Module, lazy_path: LazyPath) void {
     const b = m.owner;
     m.include_dirs.append(b.allocator, .{ .path_after = lazy_path.dupe(b) }) catch @panic("OOM");
-    addLazyPathDependenciesOnly(m, lazy_path);
 }
 
 pub fn addSystemIncludePath(m: *Module, lazy_path: LazyPath) void {
     const b = m.owner;
     m.include_dirs.append(b.allocator, .{ .path_system = lazy_path.dupe(b) }) catch @panic("OOM");
-    addLazyPathDependenciesOnly(m, lazy_path);
 }
 
 pub fn addIncludePath(m: *Module, lazy_path: LazyPath) void {
     const b = m.owner;
     m.include_dirs.append(b.allocator, .{ .path = lazy_path.dupe(b) }) catch @panic("OOM");
-    addLazyPathDependenciesOnly(m, lazy_path);
 }
 
 pub fn addConfigHeader(m: *Module, config_header: *Step.ConfigHeader) void {
     const allocator = m.owner.allocator;
     m.include_dirs.append(allocator, .{ .config_header_step = config_header }) catch @panic("OOM");
-    addStepDependenciesOnly(m, &config_header.step);
 }
 
 pub fn addSystemFrameworkPath(m: *Module, directory_path: LazyPath) void {
     const b = m.owner;
     m.include_dirs.append(b.allocator, .{ .framework_path_system = directory_path.dupe(b) }) catch
         @panic("OOM");
-    addLazyPathDependenciesOnly(m, directory_path);
 }
 
 pub fn addFrameworkPath(m: *Module, directory_path: LazyPath) void {
     const b = m.owner;
     m.include_dirs.append(b.allocator, .{ .framework_path = directory_path.dupe(b) }) catch
         @panic("OOM");
-    addLazyPathDependenciesOnly(m, directory_path);
 }
 
 pub fn addLibraryPath(m: *Module, directory_path: LazyPath) void {
     const b = m.owner;
     m.lib_paths.append(b.allocator, directory_path.dupe(b)) catch @panic("OOM");
-    addLazyPathDependenciesOnly(m, directory_path);
 }
 
 pub fn addRPath(m: *Module, directory_path: LazyPath) void {
     const b = m.owner;
     m.rpaths.append(b.allocator, .{ .lazy_path = directory_path.dupe(b) }) catch @panic("OOM");
-    addLazyPathDependenciesOnly(m, directory_path);
 }
 
 pub fn addRPathSpecial(m: *Module, bytes: []const u8) void {
@@ -784,7 +610,6 @@ fn addFlag(
 fn linkLibraryOrObject(m: *Module, other: *Step.Compile) void {
     const allocator = m.owner.allocator;
     _ = other.getEmittedBin(); // Indicate there is a dependency on the outputted binary.
-    addStepDependenciesOnly(m, &other.step);
 
     if (other.rootModuleTarget().os.tag == .windows and other.isDynamicLibrary()) {
         _ = other.getEmittedImplib(); // Indicate dependency on the outputted implib.
@@ -792,8 +617,6 @@ fn linkLibraryOrObject(m: *Module, other: *Step.Compile) void {
 
     m.link_objects.append(allocator, .{ .other_step = other }) catch @panic("OOM");
     m.include_dirs.append(allocator, .{ .other_step = other }) catch @panic("OOM");
-
-    addLazyPathDependenciesOnly(m, other.getEmittedIncludeTree());
 }
 
 fn requireKnownTarget(m: *Module) std.Target {
@@ -802,6 +625,46 @@ fn requireKnownTarget(m: *Module) std.Target {
     return resolved_target.result;
 }
 
+/// Elements of `modules` and `names` are matched one-to-one.
+pub const Graph = struct {
+    modules: []const *Module,
+    names: []const []const u8,
+};
+
+/// Intended to be used during the make phase only.
+///
+/// Given that `root` is the root `Module` of a compilation, return all `Module`s
+/// in the module graph, including `root` itself. `root` is guaranteed to be the
+/// first module in the returned slice.
+pub fn getGraph(root: *Module) Graph {
+    if (root.cached_graph.modules.len != 0) {
+        return root.cached_graph;
+    }
+
+    const arena = root.owner.graph.arena;
+
+    var modules: std.AutoArrayHashMapUnmanaged(*std.Build.Module, []const u8) = .empty;
+    var next_idx: usize = 0;
+
+    modules.putNoClobber(arena, root, "root") catch @panic("OOM");
+
+    while (next_idx < modules.count()) {
+        const mod = modules.keys()[next_idx];
+        next_idx += 1;
+        modules.ensureUnusedCapacity(arena, mod.import_table.count()) catch @panic("OOM");
+        for (mod.import_table.keys(), mod.import_table.values()) |import_name, other_mod| {
+            modules.putAssumeCapacity(other_mod, import_name);
+        }
+    }
+
+    const result: Graph = .{
+        .modules = modules.keys(),
+        .names = modules.values(),
+    };
+    root.cached_graph = result;
+    return result;
+}
+
 const Module = @This();
 const std = @import("std");
 const assert = std.debug.assert;