From a5adfda5672ef2bba9deb6eefda2952ddabb3b21 Mon Sep 17 00:00:00 2001 From: Luis Padron Date: Tue, 29 Oct 2024 21:54:43 -0400 Subject: [PATCH] feat: add support for Swift package registries --- examples/swift_package_registry/.bazelrc | 8 +++ examples/swift_package_registry/BUILD.bazel | 9 +++ examples/swift_package_registry/MODULE.bazel | 62 +++++++++++++++++++ .../swift_package_registry/Package.resolved | 14 +++++ examples/swift_package_registry/Package.swift | 10 +++ examples/swift_package_registry/README.md | 6 ++ examples/swift_package_registry/do_test | 29 +++++++++ examples/swift_package_registry/main.swift | 6 ++ swiftpkg/bzlmod/swift_deps.bzl | 9 ++- swiftpkg/internal/bazel_repo_names.bzl | 10 +++ swiftpkg/internal/pkginfos.bzl | 39 ++++++++++-- swiftpkg/internal/swift_package_tool.bzl | 58 +++++++++++++++++ .../swift_package_tool_runner_template.sh | 13 ++++ swiftpkg/internal/swift_registry_package.bzl | 32 ++++++++++ 14 files changed, 300 insertions(+), 5 deletions(-) create mode 100644 examples/swift_package_registry/.bazelrc create mode 100644 examples/swift_package_registry/BUILD.bazel create mode 100644 examples/swift_package_registry/MODULE.bazel create mode 100644 examples/swift_package_registry/Package.resolved create mode 100644 examples/swift_package_registry/Package.swift create mode 100644 examples/swift_package_registry/README.md create mode 100755 examples/swift_package_registry/do_test create mode 100644 examples/swift_package_registry/main.swift create mode 100644 swiftpkg/internal/swift_registry_package.bzl diff --git a/examples/swift_package_registry/.bazelrc b/examples/swift_package_registry/.bazelrc new file mode 100644 index 000000000..e9769fc7b --- /dev/null +++ b/examples/swift_package_registry/.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/swift_package_registry/BUILD.bazel b/examples/swift_package_registry/BUILD.bazel new file mode 100644 index 000000000..c527122e4 --- /dev/null +++ b/examples/swift_package_registry/BUILD.bazel @@ -0,0 +1,9 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SwiftPackageRegistryExample", + srcs = ["main.swift"], + deps = [ + "@swiftpkg_swift_collections//:Collections", + ], +) diff --git a/examples/swift_package_registry/MODULE.bazel b/examples/swift_package_registry/MODULE.bazel new file mode 100644 index 000000000..cce789185 --- /dev/null +++ b/examples/swift_package_registry/MODULE.bazel @@ -0,0 +1,62 @@ +module( + name = "interesting_deps_example", + version = "0.0.0", +) + +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.21.0") +bazel_dep(name = "bazel_skylib", version = "1.7.1") + +# The apple_support bazel_dep must come before the rules_cc. +# https://github.com/bazelbuild/apple_support#incompatible-toolchain-resolution +bazel_dep(name = "apple_support", version = "1.17.1") +bazel_dep( + name = "rules_swift", + version = "2.2.2", + repo_name = "build_bazel_rules_swift", +) + +bazel_dep( + name = "bazel_skylib_gazelle_plugin", + version = "1.7.1", + dev_dependency = True, +) +bazel_dep( + name = "gazelle", + version = "0.39.1", + dev_dependency = True, + repo_name = "bazel_gazelle", +) + +apple_cc_configure = use_extension( + "@apple_support//crosstool:setup.bzl", + "apple_cc_configure_extension", +) +use_repo(apple_cc_configure, "local_config_apple_cc") + +swift_deps = use_extension( + "@rules_swift_package_manager//:extensions.bzl", + "swift_deps", +) +swift_deps.from_package( + resolved = "//:Package.resolved", + swift = "//:Package.swift", +) +swift_deps.configure_swift_package( + registries = { + "[default]": "http://localhost:8000", + }, +) + +use_repo( + swift_deps, + "swift_package", +) diff --git a/examples/swift_package_registry/Package.resolved b/examples/swift_package_registry/Package.resolved new file mode 100644 index 000000000..75956a4bb --- /dev/null +++ b/examples/swift_package_registry/Package.resolved @@ -0,0 +1,14 @@ +{ + "originHash" : "21c64ff306c68b30986eb70b9aa9bb2771832a6236d332312f98a602acd95942", + "pins" : [ + { + "identity" : "apple.swift-collections", + "kind" : "registry", + "location" : "", + "state" : { + "version" : "1.1.3" + } + } + ], + "version" : 3 +} diff --git a/examples/swift_package_registry/Package.swift b/examples/swift_package_registry/Package.swift new file mode 100644 index 000000000..22a646735 --- /dev/null +++ b/examples/swift_package_registry/Package.swift @@ -0,0 +1,10 @@ +// swift-tools-version: 5.10 + +import PackageDescription + +let package = Package( + name: "SwiftPackageRegistryExample", + dependencies: [ + .package(id: "apple.swift-collections", exact: "1.1.3"), + ] +) diff --git a/examples/swift_package_registry/README.md b/examples/swift_package_registry/README.md new file mode 100644 index 000000000..bdf3a271b --- /dev/null +++ b/examples/swift_package_registry/README.md @@ -0,0 +1,6 @@ +# Example using a Swift Package Registry + +This example demonstrates how to declare dependent Swift packages that are hosted in a Swift +Package Registry. + +TODO: Add an example. diff --git a/examples/swift_package_registry/do_test b/examples/swift_package_registry/do_test new file mode 100755 index 000000000..e728d1e40 --- /dev/null +++ b/examples/swift_package_registry/do_test @@ -0,0 +1,29 @@ +#!/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}" + +# This example requires a local registry to be running on `localhost:8000`. +# As such we spin up a local registry using https://github.com/luispadron/fake-swift-package-registry +curl \ + -L https://github.com/luispadron/fake-swift-package-registry/releases/download/0.3.0/fake-swift-package-registry \ + -o /tmp/fake-swift-package-registry +chmod +x /tmp/fake-swift-package-registry +spctl --add /tmp/fake-swift-package-registry # the binary is not signed, allow it to run + +# Start the fake registry in the background +/tmp/fake-swift-package-registry > /tmp/fake-swift-package-registry.log 2>&1 & +fake_swift_package_registry_pid=$! +trap "kill ${fake_swift_package_registry_pid}" EXIT ERR + +# Test resolving the package via the `swift_package` repo. +"${bazel}" run @swift_package//:resolve + +# Ensure that it builds and tests pass +"${bazel}" test //... + +# Shutdown the fake registry +kill "${fake_swift_package_registry_pid}" diff --git a/examples/swift_package_registry/main.swift b/examples/swift_package_registry/main.swift new file mode 100644 index 000000000..07f27050a --- /dev/null +++ b/examples/swift_package_registry/main.swift @@ -0,0 +1,6 @@ +import Collections + +let orderedSet = OrderedSet([1, 2, 3, 4, 5]) + +print("Hello, world!") +print(orderedSet) diff --git a/swiftpkg/bzlmod/swift_deps.bzl b/swiftpkg/bzlmod/swift_deps.bzl index c5c83465b..f841c79dd 100644 --- a/swiftpkg/bzlmod/swift_deps.bzl +++ b/swiftpkg/bzlmod/swift_deps.bzl @@ -8,7 +8,7 @@ load("//swiftpkg/internal:swift_deps_info.bzl", "swift_deps_info") load("//swiftpkg/internal:swift_package.bzl", "PATCH_ATTRS", "swift_package") load("//swiftpkg/internal:swift_package_tool.bzl", "SWIFT_PACKAGE_CONFIG_ATTRS") load("//swiftpkg/internal:swift_package_tool_repo.bzl", "swift_package_tool_repo") - +load("//swiftpkg/internal:swift_registry_package.bzl", "swift_registry_package") # MARK: - swift_deps bzlmod Extension _DO_WHILE_RANGE = range(1000) @@ -196,6 +196,13 @@ def _declare_pkg_from_dependency(dep, config_pkg): dependencies_index = None, ) + elif dep.registry: + swift_registry_package( + name = name, + id = dep.registry.pin.identity, + version = dep.registry.pin.state.version, + ) + def _declare_swift_package_repo(name, from_package, config_swift_package): config_swift_package_kwargs = repository_utils.struct_to_kwargs( struct = config_swift_package, diff --git a/swiftpkg/internal/bazel_repo_names.bzl b/swiftpkg/internal/bazel_repo_names.bzl index 108eba263..1a3745d9a 100644 --- a/swiftpkg/internal/bazel_repo_names.bzl +++ b/swiftpkg/internal/bazel_repo_names.bzl @@ -17,6 +17,16 @@ def _from_identity(identity): Returns: A Bazel repository name as a `string`. """ + + # TODO: Should we do this replacement for registry packages? + # This potentially conflicts with Swift Package Manager's `--use-registry-identity-for-scm` flag. + # Without this though, what would have been generated as `swiftpkg_swift_collections` is instead + # `swiftpkg_apple.swift_collections` which seems more confusing and build breaking if going from registry + # to non-registry. + registry_org_identity_split = identity.split(".") + if len(registry_org_identity_split) > 1: + identity = registry_org_identity_split[1] + return "swiftpkg_" + identity.replace("-", "_") def _normalize(repo_name): diff --git a/swiftpkg/internal/pkginfos.bzl b/swiftpkg/internal/pkginfos.bzl index 08aa9a413..6c1bf1ebe 100644 --- a/swiftpkg/internal/pkginfos.bzl +++ b/swiftpkg/internal/pkginfos.bzl @@ -160,6 +160,13 @@ def _new_dependency_from_desc_json_map(dep_names_by_id, dep_map, resolved_dep_ma ) elif type == "fileSystem": file_system = _new_file_system(path = dep_map["path"]) + elif type == "registry": + pin = None + if resolved_dep_map: + pin = _new_pin_from_resolved_dep_map(resolved_dep_map) + registry = _new_registry( + pin = pin, + ) else: fail("Unrecognized dependency type {type} for {identity}.".format( type = type, @@ -171,6 +178,7 @@ def _new_dependency_from_desc_json_map(dep_names_by_id, dep_map, resolved_dep_ma name = name, source_control = source_control, file_system = file_system, + registry = registry, ) def _new_pin_from_resolved_dep_map(resolved_dep_map): @@ -180,7 +188,7 @@ def _new_pin_from_resolved_dep_map(resolved_dep_map): kind = resolved_dep_map["kind"], location = resolved_dep_map["location"], state = _new_pin_state( - revision = state_map["revision"], + revision = state_map.get("revision"), version = state_map.get("version"), ), ) @@ -499,7 +507,7 @@ def _new_dependency_identity_to_name_map(dump_deps): result = {} for dep in dump_deps: identity_provider_list = ( - dep.get("sourceControl") or dep.get("fileSystem") + dep.get("sourceControl") or dep.get("fileSystem") or dep.get("registry") ) if not identity_provider_list: continue @@ -703,7 +711,7 @@ def _new_platform(name, version): # MARK: - External Dependency -def _new_dependency(identity, name, source_control = None, file_system = None): +def _new_dependency(identity, name, source_control = None, file_system = None, registry = None): """Creates a `struct` representing an external dependency for a Swift \ package. @@ -716,6 +724,9 @@ def _new_dependency(identity, name, source_control = None, file_system = None): file_system: Optional. A `struct` as returned by `pkginfos.new_file_system()`. If present, it identifies the dependency as being loaded from a local file system. + registry: Optional. A `struct` as returned by + `pkginfos.new_registry()`. If present, it identifies the + dependency as being loaded from a Swift package registry. Returns: A `struct` representing an external dependency. @@ -726,6 +737,7 @@ def _new_dependency(identity, name, source_control = None, file_system = None): name = pkginfo_dependencies.normalize_name(name), source_control = source_control, file_system = file_system, + registry = registry, ) def _new_source_control(pin): @@ -767,8 +779,13 @@ def _new_pin(identity, kind, location, state): def _new_pin_state(revision, version = None): """Create a `struct` representing the state for a pin. + `revision` is not provided for Swift package registry pins. + `version` is not provided for git ref pins. + + It is a `fail` to not have at least one of the two fields set. + Args: - revision: The commit hash as a `string`. + revision: Optional. The commit hash as a `string`. version: Optional. The version string for the commit as a `string`. Returns: @@ -792,6 +809,19 @@ def _new_file_system(path): path = path, ) +def _new_registry(pin): + """Create a `struct` representing a registry dependency. + + Args: + pin: A `struct` as returned by `pkginfos.new_pin()`. + + Returns: + A `struct` representing a registry dependency. + """ + return struct( + pin = pin, + ) + def _new_dependency_requirement(ranges = None): """Creates a `struct` representing the requirements for an external \ dependency. @@ -1667,6 +1697,7 @@ pkginfos = struct( new_product = _new_product, new_product_reference = _new_product_reference, new_product_type = _new_product_type, + new_registry = _new_registry, new_resource = _new_resource, new_resource_rule = _new_resource_rule, new_resource_rule_process = _new_resource_rule_process, diff --git a/swiftpkg/internal/swift_package_tool.bzl b/swiftpkg/internal/swift_package_tool.bzl index aaab9c878..11e5d40a1 100644 --- a/swiftpkg/internal/swift_package_tool.bzl +++ b/swiftpkg/internal/swift_package_tool.bzl @@ -7,12 +7,34 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_common") # The name of the runner script. _RUNNER_SCRIPT_NAME = "swift_package.sh" +def _registries_json_from_dict(registries): + """Provides the JSON representation of a registries dictionary. + + Args: + registries: A dictionary of registries, keyed by their scope. + + Returns: + The Swift Package Manager compatible JSON representation of the registries dictionary. + """ + + registries = { + scope: {"url": url} + for scope, url in registries.items() + } + + return json.encode({ + "registries": registries, + "version": 1, + }) + def _swift_package_tool_impl(ctx): build_path = ctx.attr.build_path cache_path = ctx.attr.cache_path cmd = ctx.attr.cmd package = ctx.attr.package package_path = paths.dirname(package) + registries = ctx.attr.registries + registries_json = "" if not registries else _registries_json_from_dict(registries) toolchain = swift_common.get_toolchain(ctx) swift = toolchain.swift_worker @@ -28,6 +50,7 @@ def _swift_package_tool_impl(ctx): template_dict.add("%(enable_dependency_cache)s", "true" if ctx.attr.dependency_caching else "false") template_dict.add("%(manifest_cache)s", ctx.attr.manifest_cache) template_dict.add("%(security_path)s", ctx.attr.security_path) + template_dict.add("%(registries_json)s", registries_json) ctx.actions.expand_template( template = ctx.file._runner_template, @@ -72,6 +95,41 @@ SWIFT_PACKAGE_CONFIG_ATTRS = { doc = "The relative path within the runfiles tree for the security directory.", default = ".security", ), + "registries": attr.string_dict( + doc = """ +The Swift package registry URLs to use for any registry resolved dependencies, keyed by their scope. + +If provided, a `registries.json` file will be written to a `.swiftpm/configuration` directory \ +in the runfiles tree for this target. + +The first registry in the list will be used as the default registry. + +Given the following `registries` attribute: + +```python +registries = { + "[default]": "http://example.com", + "local": "http://localhost:8000", +} +``` + +Generates the following `registries.json` file: + +```json +{ + "registries" : { + "[default]" : { + "url" : "http://example.com" + }, + "local" : { + "url" : "http://localhost:8000" + } + }, + "version" : 1 +} +``` +""", + ) } swift_package_tool = rule( diff --git a/swiftpkg/internal/swift_package_tool_runner_template.sh b/swiftpkg/internal/swift_package_tool_runner_template.sh index f2e810920..08b643a51 100644 --- a/swiftpkg/internal/swift_package_tool_runner_template.sh +++ b/swiftpkg/internal/swift_package_tool_runner_template.sh @@ -27,6 +27,7 @@ enable_build_manifest_caching="%(enable_build_manifest_caching)s" enable_dependency_cache="%(enable_dependency_cache)s" manifest_cache="%(manifest_cache)s" security_path="%(security_path)s" +registries_json='%(registries_json)s' # Construct dynamic arguments. args=() @@ -45,12 +46,24 @@ fi args+=("--manifest-cache=$manifest_cache") +# TODO: make this configurable. +# AFAICT this is not configurable for registry use today. +# `swift package` has a `--config-path` argument but it doesn't seem to be used for this. +config_path=".swiftpm/configuration" +mkdir -p "$config_path" + +# If registries_json is not empty, write it to a file. +if [ -n "$registries_json" ]; then + echo "$registries_json" > "$config_path/registries.json" +fi + # Run the command. "$swift_worker" swift package \ --build-path "$build_path" \ --cache-path "$cache_path" \ --package-path "$package_path" \ --security-path "$security_path" \ + --config-path "$config_path" \ "$cmd" \ "${args[@]}" \ "$@" diff --git a/swiftpkg/internal/swift_registry_package.bzl b/swiftpkg/internal/swift_registry_package.bzl new file mode 100644 index 000000000..40dd11e7b --- /dev/null +++ b/swiftpkg/internal/swift_registry_package.bzl @@ -0,0 +1,32 @@ +"""Implementation for `swift_registry_package`.""" + +load("@bazel_skylib//lib:dicts.bzl", "dicts") +load(":repo_rules.bzl", "repo_rules") + +def _swift_registry_package_impl(repository_ctx): + fail("FOOOO", repository_ctx.attr.name) + +_REGISTRY_ATTRS = { + "id": attr.string( + mandatory = True, + doc = "The package identifier.", + ), + "version": attr.string( + mandatory = True, + doc = "The package version.", + ), +} + +_ALL_ATTRS = dicts.add( + _REGISTRY_ATTRS, + repo_rules.env_attrs, + repo_rules.swift_attrs, +) + +swift_registry_package = repository_rule( + implementation = _swift_registry_package_impl, + attrs = _ALL_ATTRS, + doc = """\ +Used to download and build an external Swift package from a registry. +""", +)