diff --git a/examples/multiple_rules_spm_example/.bazelrc b/examples/multiple_rules_spm_example/.bazelrc new file mode 100644 index 000000000..e9769fc7b --- /dev/null +++ b/examples/multiple_rules_spm_example/.bazelrc @@ -0,0 +1,8 @@ +# Import Shared settings +import %workspace%/../../shared.bazelrc + +# Import CI settings. +import %workspace%/../../ci.bazelrc + +# Try to import a local.rc file; typically, written by CI +try-import %workspace%/../../local.bazelrc diff --git a/examples/multiple_rules_spm_example/BUILD.bazel b/examples/multiple_rules_spm_example/BUILD.bazel new file mode 100644 index 000000000..e5ac14b78 --- /dev/null +++ b/examples/multiple_rules_spm_example/BUILD.bazel @@ -0,0 +1,18 @@ +load("@build_bazel_rules_apple//apple:macos.bzl", "macos_build_test") +load("@build_bazel_rules_swift//swift:swift_binary.bzl", "swift_binary") + +swift_binary( + name = "multiple_rules_spm_example", + srcs = ["main.swift"], + deps = [ + "@swiftpkg_swift_log//:Logging", + "@swiftpkg_vapor//:Vapor", + "@vapor_example//Sources/App", + ], +) + +macos_build_test( + name = "multiple_rules_spm_example_build_test", + minimum_os_version = "13.0", + targets = [":multiple_rules_spm_example"], +) diff --git a/examples/multiple_rules_spm_example/MODULE.bazel b/examples/multiple_rules_spm_example/MODULE.bazel new file mode 100644 index 000000000..f29b339ea --- /dev/null +++ b/examples/multiple_rules_spm_example/MODULE.bazel @@ -0,0 +1,54 @@ +bazel_dep( + name = "rules_swift_package_manager", + version = "0.0.0", +) +local_path_override( + module_name = "rules_swift_package_manager", + path = "../..", +) + +bazel_dep(name = "cgrindel_bazel_starlib", version = "0.27.0") +bazel_dep(name = "bazel_skylib", version = "1.8.2") +bazel_dep(name = "apple_support", version = "1.24.4") +bazel_dep( + name = "rules_swift", + version = "3.2.0", + repo_name = "build_bazel_rules_swift", +) +bazel_dep( + name = "rules_apple", + version = "4.2.0", + repo_name = "build_bazel_rules_apple", +) + +bazel_dep( + name = "bazel_skylib_gazelle_plugin", + version = "1.8.2", + dev_dependency = True, +) + +swift_deps = use_extension( + "@rules_swift_package_manager//:extensions.bzl", + "swift_deps", +) +swift_deps.from_package( + check_direct_dependencies = True, + resolved = "//:Package.resolved", + swift = "//:Package.swift", +) +use_repo( + swift_deps, + "swift_package", + "swiftpkg_swift_log", + "swiftpkg_vapor", +) + +# Depend on another repository via Bzlmod that uses `rules_swift_package_manager` to declare its dependencies +bazel_dep( + name = "vapor_example", + version = "0.0.0", +) +local_path_override( + module_name = "vapor_example", + path = "../vapor_example", +) diff --git a/examples/multiple_rules_spm_example/Package.resolved b/examples/multiple_rules_spm_example/Package.resolved new file mode 100644 index 000000000..d67e3c39f --- /dev/null +++ b/examples/multiple_rules_spm_example/Package.resolved @@ -0,0 +1,257 @@ +{ + "pins" : [ + { + "identity" : "async-http-client", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/async-http-client.git", + "state" : { + "revision" : "efb14fec9f79f3f8d4f2a6c0530303efb6fe6533", + "version" : "1.29.1" + } + }, + { + "identity" : "async-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/async-kit.git", + "state" : { + "revision" : "6f3615ccf2ac3c2ae0c8087d527546e9544a43dd", + "version" : "1.21.0" + } + }, + { + "identity" : "console-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/console-kit.git", + "state" : { + "revision" : "742f624a998cba2a9e653d9b1e91ad3f3a5dff6b", + "version" : "4.15.2" + } + }, + { + "identity" : "multipart-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/multipart-kit.git", + "state" : { + "revision" : "3498e60218e6003894ff95192d756e238c01f44e", + "version" : "4.7.1" + } + }, + { + "identity" : "routing-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/routing-kit.git", + "state" : { + "revision" : "93f7222c8e195cbad39fafb5a0e4cc85a8def7ea", + "version" : "4.9.2" + } + }, + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms.git", + "state" : { + "revision" : "87e50f483c54e6efd60e885f7f5aa946cee68023", + "version" : "1.2.1" + } + }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "40d25bbb2fc5b557a9aa8512210bded327c0f60d", + "version" : "1.5.0" + } + }, + { + "identity" : "swift-async-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-async-algorithms.git", + "state" : { + "revision" : "042e1c4d9d19748c9c228f8d4ebc97bb1e339b0b", + "version" : "1.0.4" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-certificates", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-certificates.git", + "state" : { + "revision" : "c399f90e7bbe8874f6cbfda1d5f9023d1f5ce122", + "version" : "1.15.1" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "e8ed8867ec23bccf5f3bb9342148fa8deaff9b49", + "version" : "4.1.0" + } + }, + { + "identity" : "swift-distributed-tracing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-distributed-tracing.git", + "state" : { + "revision" : "baa932c1336f7894145cbaafcd34ce2dd0b77c97", + "version" : "1.3.1" + } + }, + { + "identity" : "swift-http-structured-headers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-structured-headers.git", + "state" : { + "revision" : "a9f3c352f4d46afd155e00b3c6e85decae6bcbeb", + "version" : "1.5.0" + } + }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types.git", + "state" : { + "revision" : "45eb0224913ea070ec4fba17291b9e7ecf4749ca", + "version" : "1.5.1" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log", + "state" : { + "revision" : "ce592ae52f982c847a4efc0dd881cc9eb32d29f2", + "version" : "1.6.4" + } + }, + { + "identity" : "swift-metrics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-metrics.git", + "state" : { + "revision" : "0743a9364382629da3bf5677b46a2c4b1ce5d2a6", + "version" : "2.7.1" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "a24771a4c228ff116df343c85fcf3dcfae31a06c", + "version" : "2.88.0" + } + }, + { + "identity" : "swift-nio-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-extras.git", + "state" : { + "revision" : "7ee281d816fa8e5f3967a2c294035a318ea551c7", + "version" : "1.31.0" + } + }, + { + "identity" : "swift-nio-http2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-http2.git", + "state" : { + "revision" : "5e9e99ec96c53bc2c18ddd10c1e25a3cd97c55e5", + "version" : "1.38.0" + } + }, + { + "identity" : "swift-nio-ssl", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-ssl.git", + "state" : { + "revision" : "173cc69a058623525a58ae6710e2f5727c663793", + "version" : "2.36.0" + } + }, + { + "identity" : "swift-nio-transport-services", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-transport-services.git", + "state" : { + "revision" : "df6c28355051c72c884574a6c858bc54f7311ff9", + "version" : "1.25.2" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics.git", + "state" : { + "revision" : "0c0290ff6b24942dadb83a929ffaaa1481df04a2", + "version" : "1.1.1" + } + }, + { + "identity" : "swift-service-context", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-service-context.git", + "state" : { + "revision" : "1983448fefc717a2bc2ebde5490fe99873c5b8a6", + "version" : "1.2.1" + } + }, + { + "identity" : "swift-service-lifecycle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/swift-service-lifecycle.git", + "state" : { + "revision" : "1de37290c0ab3c5a96028e0f02911b672fd42348", + "version" : "2.9.1" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "395a77f0aa927f0ff73941d7ac35f2b46d47c9db", + "version" : "1.6.3" + } + }, + { + "identity" : "vapor", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/vapor.git", + "state" : { + "revision" : "ac3aeb7730b63f4f54248603c38137b551b465c7", + "version" : "4.119.1" + } + }, + { + "identity" : "websocket-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/websocket-kit.git", + "state" : { + "revision" : "8666c92dbbb3c8eefc8008c9c8dcf50bfd302167", + "version" : "2.16.1" + } + } + ], + "version" : 2 +} diff --git a/examples/multiple_rules_spm_example/Package.swift b/examples/multiple_rules_spm_example/Package.swift new file mode 100644 index 000000000..15901bbc5 --- /dev/null +++ b/examples/multiple_rules_spm_example/Package.swift @@ -0,0 +1,11 @@ +// swift-tools-version: 5.7 + +import PackageDescription + +let package = Package( + name: "multiple_rules_spm_example", + dependencies: [ + .package(url: "https://github.com/apple/swift-log", from: "1.6.0"), + .package(url: "https://github.com/vapor/vapor.git", exact: "4.119.1"), + ] +) diff --git a/examples/multiple_rules_spm_example/README.md b/examples/multiple_rules_spm_example/README.md new file mode 100644 index 000000000..ad4a146e9 --- /dev/null +++ b/examples/multiple_rules_spm_example/README.md @@ -0,0 +1,3 @@ +# multiple_rules_spm Example + +This example demonstrates how one repository: `multiple_rules_spm` can depend on another repository: `vapor_example` via Bzlmod without issues. diff --git a/examples/multiple_rules_spm_example/WORKSPACE b/examples/multiple_rules_spm_example/WORKSPACE new file mode 100644 index 000000000..d251ab7ba --- /dev/null +++ b/examples/multiple_rules_spm_example/WORKSPACE @@ -0,0 +1 @@ +# Intentionally blank: Using bzlmod diff --git a/examples/multiple_rules_spm_example/WORKSPACE.bzlmod b/examples/multiple_rules_spm_example/WORKSPACE.bzlmod new file mode 100644 index 000000000..af8a0e896 --- /dev/null +++ b/examples/multiple_rules_spm_example/WORKSPACE.bzlmod @@ -0,0 +1 @@ +# Intentionally blank: Force bzlmod to strict mode diff --git a/examples/multiple_rules_spm_example/do_test b/examples/multiple_rules_spm_example/do_test new file mode 100755 index 000000000..39e06be03 --- /dev/null +++ b/examples/multiple_rules_spm_example/do_test @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -o errexit -o nounset -o pipefail + +# Use the Bazel binary specified by the integration test. Otherise, fall back +# to bazel. +bazel="${BIT_BAZEL_BINARY:-bazel}" + +# Ensure that it builds and tests pass +"${bazel}" test //... diff --git a/examples/multiple_rules_spm_example/main.swift b/examples/multiple_rules_spm_example/main.swift new file mode 100644 index 000000000..16bc9bfc4 --- /dev/null +++ b/examples/multiple_rules_spm_example/main.swift @@ -0,0 +1,13 @@ +import App +import Logging +import Vapor + +let logger = Logger(label: "com.example.main") +logger.info("Starting application...") + +var env = try Environment.detect() +try LoggingSystem.bootstrap(from: &env) +let app = Application(env) +defer { app.shutdown() } +try configure(app) +try app.run() diff --git a/examples/vapor_example/MODULE.bazel b/examples/vapor_example/MODULE.bazel index 26dfeada0..3113f325b 100644 --- a/examples/vapor_example/MODULE.bazel +++ b/examples/vapor_example/MODULE.bazel @@ -1,3 +1,7 @@ +module( + name = "vapor_example", +) + bazel_dep( name = "rules_swift_package_manager", version = "0.0.0", @@ -52,14 +56,17 @@ swift_deps = use_extension( "swift_deps", ) swift_deps.from_package( - declare_swift_deps_info = True, + # TODO: Fix before merging, these need to be disabled as they get generated + # more than once if consumed as a bzlmod dependency. + # We neeed to re-design how the swift_packge and swift_deps_info repositories are + # generated so that they are marked as dev dependencies and not root module dependencies. + declare_swift_deps_info = False, + declare_swift_package = False, resolved = "//swift:Package.resolved", swift = "//swift:Package.swift", ) use_repo( swift_deps, - "swift_deps_info", - "swift_package", "swiftpkg_fluent", "swiftpkg_fluent_sqlite_driver", "swiftpkg_vapor", diff --git a/swiftpkg/bzlmod/swift_deps.bzl b/swiftpkg/bzlmod/swift_deps.bzl index d221e912a..a3b8a3082 100644 --- a/swiftpkg/bzlmod/swift_deps.bzl +++ b/swiftpkg/bzlmod/swift_deps.bzl @@ -1,6 +1,7 @@ """Implementation for `swift_deps` bzlmod extension.""" load("@bazel_skylib//lib:dicts.bzl", "dicts") +load("@bazel_skylib//lib:versions.bzl", "versions") load("//swiftpkg/internal:bazel_repo_names.bzl", "bazel_repo_names") load("//swiftpkg/internal:local_swift_package.bzl", "local_swift_package") load("//swiftpkg/internal:pkginfos.bzl", "pkginfos") @@ -15,14 +16,111 @@ load("//swiftpkg/internal:swift_package_tool_repo.bzl", "swift_package_tool_repo _DO_WHILE_RANGE = range(1000) -def _declare_pkgs_from_package(module_ctx, from_package, config_pkgs, config_swift_package): - """Declare Swift packages from `Package.swift` and `Package.resolved`. +# MARK: - Version Comparison Helpers + +def _get_dependency_version(dep): + """Extract version string from a dependency. + + Args: + dep: A dependency struct as returned by `pkginfos.new_dependency()`. + + Returns: + The version string if available, otherwise `None`. + """ + if dep.source_control and dep.source_control.pin and dep.source_control.pin.state: + return dep.source_control.pin.state.version + if dep.registry and dep.registry.pin and dep.registry.pin.state: + return dep.registry.pin.state.version + return None + +def _is_local_package(dep): + """Check if a dependency is a local (fileSystem) package. + + Args: + dep: A dependency struct as returned by `pkginfos.new_dependency()`. + + Returns: + `True` if the dependency is a local package, otherwise `False`. + """ + return dep.file_system != None + +def _compare_versions(v1, v2): + """Compare two version strings using semantic versioning. + + Uses Minimal Version Selection (MVS) approach - selects highest version. + + Args: + v1: First version string. + v2: Second version string. + + Returns: + -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2. + """ + if v1 == v2: + return 0 + # Use bazel_skylib versions module for comparison + if versions.is_at_least(threshold = v1, version = v2): + # v2 >= v1 and v1 != v2 (already checked above), so v2 > v1 + return -1 + else: + # v2 < v1, so v1 > v2 + return 1 + +def _compare_dependencies(dep1, dep2): + """Compare two dependencies to determine which should be selected. + + Uses the following priority: + 1. Local packages always win over remote packages + 2. For remote packages, highest version wins (MVS) + 3. If versions are equal or both None, prefer the first one + + Args: + dep1: First dependency candidate struct. + dep2: Second dependency candidate struct. + + Returns: + -1 if dep1 < dep2 (dep2 should be selected), 0 if equal, 1 if dep1 > dep2 (dep1 should be selected). + """ + is_local1 = _is_local_package(dep1.dep) + is_local2 = _is_local_package(dep2.dep) + + # Local packages always win + if is_local1 and not is_local2: + return 1 + if is_local2 and not is_local1: + return -1 + if is_local1 and is_local2: + # Both are local - prefer first one + return 0 + + # Both are remote - compare versions + v1 = _get_dependency_version(dep1.dep) + v2 = _get_dependency_version(dep2.dep) + return _compare_versions(v1, v2) + + +def _collect_dependencies_from_package( + module_ctx, + module, + from_package, + config_pkgs, + config_swift_package): + """Collect all Swift package dependencies from a `Package.swift` and `Package.resolved`. + + This function collects dependencies but does not declare repositories yet. + This allows us to resolve duplicates across all modules before declaring. Args: module_ctx: An instance of `module_ctx`. + module: The bazel_module object for tracking which module declared the dependency. from_package: The data from the `from_package` tag. config_pkgs: The data from the `configure_package` tag. config_swift_package: The data from the `configure_swift_package` tag. + + Returns: + A tuple of (all_deps_list, direct_dep_repo_names_list) where: + - all_deps_list: List of dependency candidate structs with all transitive deps + - direct_dep_repo_names_list: List of bazel repo names for direct dependencies """ # Read Package.resolved. @@ -99,24 +197,6 @@ the Swift package to make it available.\ pkg_info_label = "@{}//:pkg_info.json".format(bazel_repo_name) direct_dep_pkg_infos[pkg_info_label] = dep.identity - # Write info about the Swift deps that may be used by external tooling. - if from_package.declare_swift_deps_info: - swift_deps_info_repo_name = "swift_deps_info" - swift_deps_info( - name = swift_deps_info_repo_name, - direct_dep_pkg_infos = direct_dep_pkg_infos, - ) - direct_dep_repo_names.append(swift_deps_info_repo_name) - - if from_package.declare_swift_package: - swift_package_repo_name = "swift_package" - _declare_swift_package_repo( - name = swift_package_repo_name, - from_package = from_package, - config_swift_package = config_swift_package, - ) - direct_dep_repo_names.append(swift_package_repo_name) - # Ensure that we add all of the transitive source control # or registry deps from the resolved file. for pin_map in resolved_pkg_map.get("pins", []): @@ -178,26 +258,101 @@ the Swift package to make it available.\ if to_process: fail("Expected no more items to process, but found some.") - # Declare the Bazel repositories. + # Create dependency candidates for all dependencies + all_dep_candidates = [] for dep in all_deps_by_id.values(): + bazel_repo_name = bazel_repo_names.from_identity(dep.identity) config_pkg = config_pkgs.get(dep.name) if config_pkg == None: - config_pkg = config_pkgs.get( - bazel_repo_names.from_identity(dep.identity), - ) - _declare_pkg_from_dependency(dep, config_pkg, from_package, config_swift_package) - - # Add all transitive dependencies to direct_dep_repo_names if `publicly_expose_all_targets` flag is set. - for dep in all_deps_by_id.values(): - config_pkg = config_pkgs.get(dep.name) or config_pkgs.get( - bazel_repo_names.from_identity(dep.identity), + config_pkg = config_pkgs.get(bazel_repo_name) + dep_candidate = struct( + dep = dep, + bazel_repo_name = bazel_repo_name, + config_pkg = config_pkg, + from_package = from_package, + config_swift_package = config_swift_package, + module = module, + direct_dep = bazel_repo_name in direct_dep_repo_names, + publicly_expose_all_targets = config_pkg and config_pkg.publicly_expose_all_targets or False, ) - if config_pkg and config_pkg.publicly_expose_all_targets: - bazel_repo_name = bazel_repo_names.from_identity(dep.identity) - if bazel_repo_name not in direct_dep_repo_names: - direct_dep_repo_names.append(bazel_repo_name) + all_dep_candidates.append(dep_candidate) + + return (all_dep_candidates, direct_dep_repo_names) - return direct_dep_repo_names +def _resolve_duplicate_dependencies(all_dep_candidates, check_direct_dependencies, root_module_declared_versions): + """Resolve duplicate dependencies by selecting the best version. + + Uses Minimal Version Selection (MVS) - selects highest version. + Local packages always take precedence over remote packages. + + Args: + all_dep_candidates: List of all dependency candidate structs from all modules. + check_direct_dependencies: Boolean indicating whether to check version conflicts. + root_module_declared_versions: Dict mapping bazel_repo_name to version string + for dependencies declared in the root module. + + Returns: + A dict mapping bazel_repo_name to the selected dependency candidate. + """ + # Group dependencies by repository name + deps_by_repo_name = {} + for candidate in all_dep_candidates: + repo_name = candidate.bazel_repo_name + if repo_name not in deps_by_repo_name: + deps_by_repo_name[repo_name] = [] + deps_by_repo_name[repo_name].append(candidate) + + # Resolve duplicates + resolved_deps = {} + for repo_name, candidates in deps_by_repo_name.items(): + if len(candidates) == 1: + # No duplicates, use the single candidate + resolved_deps[repo_name] = candidates[0] + else: + # Multiple candidates - resolve duplicates using MVS + selected = candidates[0] + for candidate in candidates[1:]: + comparison = _compare_dependencies(selected, candidate) + if comparison < 0: + # candidate is better than selected + selected = candidate + + resolved_deps[repo_name] = selected + + # Check for version mismatches for root module direct dependencies only + if check_direct_dependencies: + for repo_name, declared_version in root_module_declared_versions.items(): + resolved_candidate = resolved_deps.get(repo_name) + if resolved_candidate == None: + continue + resolved_version = _get_dependency_version(resolved_candidate.dep) + if resolved_version and declared_version and resolved_version != declared_version: + fail(""" +For repository '{repo}', root module declared {repo}@{declared} but {repo}@{resolved} \ +from '{resolved_module}' was selected in the resolved dependency graph. \ +To fix: Update your Package.swift to use version >= {resolved}, or set check_direct_dependencies = False. +""".format( + repo = repo_name, + declared = declared_version, + resolved = resolved_version, + resolved_module = resolved_candidate.module.name, + )) + + return resolved_deps + +def _declare_resolved_dependencies(resolved_deps): + """Declare Bazel repositories for resolved dependencies. + + Args: + resolved_deps: Dict mapping bazel_repo_name to selected dependency candidate. + """ + for candidate in resolved_deps.values(): + _declare_pkg_from_dependency( + candidate.dep, + candidate.config_pkg, + candidate.from_package, + candidate.config_swift_package, + ) def _declare_pkg_from_dependency(dep, config_pkg, from_package, config_swift_package): name = bazel_repo_names.from_identity(dep.identity) @@ -296,6 +451,7 @@ def _declare_swift_package_repo(name, from_package, config_swift_package): ) def _swift_deps_impl(module_ctx): + # Collect configuration config_pkgs = {} for mod in module_ctx.modules: for config_pkg in mod.tags.configure_package: @@ -308,17 +464,111 @@ def _swift_deps_impl(module_ctx): Expected only one `configure_swift_package` tag, but found multiple.\ """) config_swift_package = config_swift_package_tag - direct_dep_repo_names = [] + + # Phase 1: Collect all dependencies from all modules + all_dep_candidates = [] + root_module_direct_dep_repo_names = [] + root_module_declared_versions = {} # Track declared versions for root module dependencies + root_declare_swift_deps_info = False + root_declare_swift_package = False + swift_deps_info_repos = [] + swift_package_repos = [] + check_direct_dependencies = False + for mod in module_ctx.modules: for from_package in mod.tags.from_package: - direct_dep_repo_names.extend( - _declare_pkgs_from_package( - module_ctx, - from_package, - config_pkgs, - config_swift_package, - ), + # Enable checking if the root module enables it + if mod.is_root and from_package.check_direct_dependencies: + check_direct_dependencies = True + + # Collect dependencies from this module + (module_dep_candidates, module_direct_dep_repo_names) = _collect_dependencies_from_package( + module_ctx, + mod, + from_package, + config_pkgs, + config_swift_package, ) + all_dep_candidates.extend(module_dep_candidates) + # Track root module direct dependencies and their declared versions + if mod.is_root: + root_module_direct_dep_repo_names.extend(module_direct_dep_repo_names) + # Store declared versions for root module direct dependencies + # (versions are already captured in module_dep_candidates) + for candidate in module_dep_candidates: + if candidate.direct_dep: + version = _get_dependency_version(candidate.dep) + if version: + root_module_declared_versions[candidate.bazel_repo_name] = version + # Track root module's declaration flags + if from_package.declare_swift_deps_info: + root_declare_swift_deps_info = True + if from_package.declare_swift_package: + root_declare_swift_package = True + + # Handle swift_deps_info and swift_package repositories + if from_package.declare_swift_deps_info: + # Collect direct dep pkg infos for this module + direct_dep_pkg_infos = {} + for candidate in module_dep_candidates: + if candidate.direct_dep: + pkg_info_label = "@{}//:pkg_info.json".format(candidate.bazel_repo_name) + direct_dep_pkg_infos[pkg_info_label] = candidate.dep.identity + swift_deps_info_repos.append((from_package, direct_dep_pkg_infos)) + + if from_package.declare_swift_package: + swift_package_repos.append((from_package, config_swift_package)) + + # Phase 2: Resolve duplicates + resolved_deps = _resolve_duplicate_dependencies(all_dep_candidates, check_direct_dependencies, root_module_declared_versions) + + # Phase 3: Declare repositories + _declare_resolved_dependencies(resolved_deps) + + # Declare swift_deps_info repository (only once, prefer root module) + if swift_deps_info_repos: + # Merge direct_dep_pkg_infos from all modules + merged_direct_dep_pkg_infos = {} + for (_, direct_dep_pkg_infos) in swift_deps_info_repos: + merged_direct_dep_pkg_infos.update(direct_dep_pkg_infos) + swift_deps_info_repo_name = "swift_deps_info" + swift_deps_info( + name = swift_deps_info_repo_name, + direct_dep_pkg_infos = merged_direct_dep_pkg_infos, + ) + + # Declare swift_package repository (only once, prefer root module) + if swift_package_repos: + # Use the first one (typically root module) + (from_package_for_repo, config_swift_package_for_repo) = swift_package_repos[0] + swift_package_repo_name = "swift_package" + _declare_swift_package_repo( + name = swift_package_repo_name, + from_package = from_package_for_repo, + config_swift_package = config_swift_package_for_repo, + ) + + # Build final direct_dep_repo_names list - only root module direct dependencies + direct_dep_repo_names = [] + + # Add root module direct dependencies that are resolved + for repo_name in root_module_direct_dep_repo_names: + if repo_name in resolved_deps: + direct_dep_repo_names.append(repo_name) + + # Add transitive dependencies if publicly_expose_all_targets is set + for candidate in resolved_deps.values(): + if candidate.publicly_expose_all_targets: + repo_name = candidate.bazel_repo_name + if repo_name not in direct_dep_repo_names: + direct_dep_repo_names.append(repo_name) + + # Add swift_deps_info and swift_package if declared by root module + if root_declare_swift_deps_info: + direct_dep_repo_names.append("swift_deps_info") + if root_declare_swift_package: + direct_dep_repo_names.append("swift_package") + return module_ctx.extension_metadata( root_module_direct_deps = direct_dep_repo_names, root_module_direct_dev_deps = [], @@ -391,6 +641,14 @@ in the output log. allow_files = [".swift"], doc = "A `Package.swift`.", ), + "check_direct_dependencies": attr.bool( + default = True, + doc = """\ +Check if the direct dependencies declared in the root module match the versions in the \ +resolved dependency graph. When enabled, the build will fail if the root module's \ +Package.resolved contains different versions than what MVS selected. Defaults to True.\ +""", + ), }, ), doc = "Load Swift packages from `Package.swift` and `Package.resolved` files.",