diff --git a/doc/package_descriptions_api.md b/doc/package_descriptions_api.md index 8330476b..97f9d99b 100755 --- a/doc/package_descriptions_api.md +++ b/doc/package_descriptions_api.md @@ -285,3 +285,41 @@ Returns all of the targets that are a transitive dependency for the specified pr | product_refs | A list of reference string values as created by references.create_product_ref(). | none | + + +## package_descriptions.is_executable_product + +
+package_descriptions.is_executable_product(product)
+
+ +Returns a boolean indicating whether the specified product dictionary is an executable product. + +**PARAMETERS** + + +| Name | Description | Default Value | +| :------------- | :------------- | :------------- | +| product | A dict representing a product from package description JSON. | none | + + + + +## package_descriptions.get_product + +
+package_descriptions.get_product(pkg_desc, product_name, fail_if_not_found)
+
+ +Returns the product with the specified product name. + +**PARAMETERS** + + +| Name | Description | Default Value | +| :------------- | :------------- | :------------- | +| pkg_desc | A dict representing a package description. | none | +| product_name | The product name as a string. | none | +| fail_if_not_found |

-

| True | + + diff --git a/doc/providers.md b/doc/providers.md index 5f6a992f..0323ba2c 100755 --- a/doc/providers.md +++ b/doc/providers.md @@ -16,7 +16,7 @@ On this page: ## SPMPackageInfo
-SPMPackageInfo(name, swift_modules, clang_modules, system_library_modules)
+SPMPackageInfo(name, swift_binaries, swift_modules, clang_modules, system_library_modules)
 
Describes the information about an SPM package. @@ -27,6 +27,7 @@ Describes the information about an SPM package. | Name | Description | | :------------- | :------------- | | name | Name of the Swift package. | +| swift_binaries | A list of values returned from providers.swift_binary. | | swift_modules | A list of values returned from providers.swift_module. | | clang_modules | A list of values returned from providers.clang_module. | | system_library_modules | List of values returned from providers.system_library_module. | diff --git a/doc/providers_api.md b/doc/providers_api.md index 3ef3b513..58880cfc 100755 --- a/doc/providers_api.md +++ b/doc/providers_api.md @@ -42,6 +42,26 @@ Creates a value describing a copy operation. | dest | The destination file. | none | + + +## providers.swift_binary + +
+providers.swift_binary(name, executable, all_outputs)
+
+ +Creates a value representing a Swift binary that is built from a package. + +**PARAMETERS** + + +| Name | Description | Default Value | +| :------------- | :------------- | :------------- | +| name | Name of the Swift binary. | none | +| executable | The executable. | None | +| all_outputs | All of the output files that are declared for the module. | [] | + + ## providers.swift_module diff --git a/doc/references_api.md b/doc/references_api.md index 4f69a30a..291b79ca 100755 --- a/doc/references_api.md +++ b/doc/references_api.md @@ -103,3 +103,22 @@ Returns a boolean indicating whether the reference string is a target reference. | for_pkg | Optional. A package name as a string value to include in the check. | None | + + +## references.is_product_ref + +
+references.is_product_ref(ref_str, for_pkg)
+
+ +Returns a boolean indicating whether the reference string is a product reference. + +**PARAMETERS** + + +| Name | Description | Default Value | +| :------------- | :------------- | :------------- | +| ref_str | A valid reference string. | none | +| for_pkg | Optional. A package name as a string value to include in the check. | None | + + diff --git a/examples/simple_with_binary/BUILD.bazel b/examples/simple_with_binary/BUILD.bazel index 912b1895..fe9d3abb 100644 --- a/examples/simple_with_binary/BUILD.bazel +++ b/examples/simple_with_binary/BUILD.bazel @@ -30,3 +30,18 @@ sh_test( ], deps = ["@bazel_tools//tools/bash/runfiles"], ) + +# Make sure that the binary is referenced. Otherwise, Bazel won't build it. +alias( + name = "swiftformat", + actual = "@swift_utils//SwiftFormat:swiftformat", +) + +sh_test( + name = "swiftformat_test", + srcs = ["swiftformat_test.sh"], + data = [ + ":swiftformat", + ], + deps = ["@bazel_tools//tools/bash/runfiles"], +) diff --git a/examples/simple_with_binary/WORKSPACE b/examples/simple_with_binary/WORKSPACE index 36428021..fac46c85 100644 --- a/examples/simple_with_binary/WORKSPACE +++ b/examples/simple_with_binary/WORKSPACE @@ -55,6 +55,13 @@ spm_repositories( from_version = "0.0.0", products = ["swiftlint"], ), + spm_pkg( + "https://github.com/nicklockwood/SwiftFormat.git", + from_version = "0.0.0", + products = [ + "swiftformat", + ], + ), ], platforms = [ ".macOS(.v10_12)", diff --git a/examples/simple_with_binary/swiftformat_test.sh b/examples/simple_with_binary/swiftformat_test.sh new file mode 100755 index 00000000..ddc5f1c0 --- /dev/null +++ b/examples/simple_with_binary/swiftformat_test.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +# --- begin runfiles.bash initialization v2 --- +# Copy-pasted from the Bazel Bash runfiles library v2. +set -uo pipefail; f=bazel_tools/tools/bash/runfiles/runfiles.bash +source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \ + source "$0.runfiles/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ + { echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e +# --- end runfiles.bash initialization v2 --- + +swiftformat="$(rlocation "swift_utils/SwiftFormat/swiftformat")" + +# Make sure that we can execute swiftformat. +"${swiftformat}" --version diff --git a/spm/internal/package_descriptions.bzl b/spm/internal/package_descriptions.bzl index 84311762..11a1222f 100644 --- a/spm/internal/package_descriptions.bzl +++ b/spm/internal/package_descriptions.bzl @@ -128,6 +128,18 @@ def _get_package_description(repository_ctx, working_directory = ""): # MARK: - Product Functions +def _is_executable_product(product): + """Returns a boolean indicating whether the specified product dictionary is an executable product. + + Args: + product: A `dict` representing a product from package description + JSON. + + Returns: + A `bool` indicating whether the product is an executable. + """ + return "executable" in product["type"] + def _is_library_product(product): """Returns a boolean indicating whether the specified product dictionary is a library product. @@ -525,6 +537,9 @@ package_descriptions = struct( dependency_repository_name = _dependency_repository_name, # Transitive Dependency Functions transitive_dependencies = _transitive_dependencies, + # Product Functions + is_executable_product = _is_executable_product, + get_product = _get_product, # Constants root_pkg_name = "_root", ) diff --git a/spm/internal/providers.bzl b/spm/internal/providers.bzl index 4cacb3cb..e91f7a46 100644 --- a/spm/internal/providers.bzl +++ b/spm/internal/providers.bzl @@ -39,12 +39,30 @@ SPMPackageInfo = provider( doc = "Describes the information about an SPM package.", fields = { "name": "Name of the Swift package.", + "swift_binaries": "A `list` of values returned from `providers.swift_binary`.", "swift_modules": "A `list` of values returned from `providers.swift_module`.", "clang_modules": "A `list` of values returned from `providers.clang_module`.", "system_library_modules": "`List` of values returned from `providers.system_library_module`.", }, ) +def _create_swift_binary(name, executable = None, all_outputs = []): + """Creates a value representing a Swift binary that is built from a package. + + Args: + name: Name of the Swift binary. + executable: The executable. + all_outputs: All of the output files that are declared for the module. + + Returns: + A struct which provides info about a Swift binary built by SPM. + """ + return struct( + name = name, + executable = executable, + all_outputs = all_outputs, + ) + def _create_swift_module( module_name, o_files = [], @@ -149,6 +167,7 @@ def _create_copy_info(src, dest): providers = struct( clang_module = _create_clang_module, copy_info = _create_copy_info, + swift_binary = _create_swift_binary, swift_module = _create_swift_module, system_library_module = _create_system_library_module, ) diff --git a/spm/internal/references.bzl b/spm/internal/references.bzl index 75fa753c..288e14a6 100644 --- a/spm/internal/references.bzl +++ b/spm/internal/references.bzl @@ -86,6 +86,23 @@ def _is_target_ref(ref_str, for_pkg = None): starts_with_parts.extend([for_pkg, "/"]) return ref_str.startswith("".join(starts_with_parts)) +def _is_product_ref(ref_str, for_pkg = None): + """Returns a boolean indicating whether the reference string is a product reference. + + Args: + ref_str: A valid reference `string`. + for_pkg: Optional. A package name as a `string` value to include in + the check. + + Returns: + A `bool` value indicating whether the reference string is a product + reference. + """ + starts_with_parts = [reference_types.product, ":"] + if for_pkg != None: + starts_with_parts.extend([for_pkg, "/"]) + return ref_str.startswith("".join(starts_with_parts)) + # MARK: - Namespace reference_types = struct( @@ -99,4 +116,5 @@ references = struct( create_target_ref = _create_target_ref, create_product_ref = _create_product_ref, is_target_ref = _is_target_ref, + is_product_ref = _is_product_ref, ) diff --git a/spm/internal/spm_package.bzl b/spm/internal/spm_package.bzl index e8ca7476..3f20c768 100644 --- a/spm/internal/spm_package.bzl +++ b/spm/internal/spm_package.bzl @@ -48,14 +48,11 @@ def _declare_swift_library_target_files(ctx, target, build_config_path): all_outputs = all_build_outs, ) -def _declare_swift_executable_target_files(ctx, target, build_config_path): - target_name = target["name"] - module_name = target["name"] - - executable = ctx.actions.declare_file("%s/%s" % (build_config_path, target_name)) - - return providers.swift_module( - module_name = module_name, +def _declare_swift_executable_product_files(ctx, product, build_config_path): + product_name = product["name"] + executable = ctx.actions.declare_file("%s/%s" % (build_config_path, product_name)) + return providers.swift_binary( + name = product_name, executable = executable, all_outputs = [executable], ) @@ -158,6 +155,7 @@ def _gather_package_build_info( build_config_path, clang_custom_infos_dict, pkg_desc, + product_refs, target_refs): """Gathers build information for a Swift package. @@ -168,6 +166,7 @@ def _gather_package_build_info( for this package and the values are a `list` of public headers. pkg_desc: A `dict` representing the package descprtion JSON. + product_refs: A `list` of product references (`string`) for this package. target_refs: A `list` of target references (`string`) that are dependencies for the package. @@ -176,11 +175,30 @@ def _gather_package_build_info( the build information for the Swift package. """ build_outs = [] + swift_binaries = [] swift_modules = [] clang_modules = [] system_library_modules = [] pkg_name = pkg_desc["name"] + # Collect the executable products + exec_products = [] + for product_ref in product_refs: + ref_type, pkg_name, product_name = refs.split(product_ref) + product = pds.get_product(pkg_desc, product_name) + if pds.is_executable_product(product): + exec_products.append(product) + + # Declare the outputs for all of the Swift binaries + for product in exec_products: + swift_binary_info = _declare_swift_executable_product_files( + ctx, + product, + build_config_path, + ) + swift_binaries.append(swift_binary_info) + build_outs.extend(swift_binary_info.all_outputs) + # Declare outputs for the targets that will be used for target_ref in target_refs: ref_type, pname, target_name = refs.split(target_ref) @@ -196,13 +214,9 @@ def _gather_package_build_info( swift_modules.append(swift_module_info) build_outs.extend(swift_module_info.all_outputs) elif pds.is_executable_target(target): - swift_module_info = _declare_swift_executable_target_files( - ctx, - target, - build_config_path, - ) - swift_modules.append(swift_module_info) - build_outs.extend(swift_module_info.all_outputs) + # Executable targets are declared by product, not by SPM target. + pass + else: fail("Unrecognized Swift target type. %s" % (target)) @@ -230,6 +244,7 @@ def _gather_package_build_info( pkg_info = SPMPackageInfo( name = pkg_name, + swift_binaries = swift_binaries, swift_modules = swift_modules, clang_modules = clang_modules, system_library_modules = system_library_modules, @@ -478,11 +493,13 @@ def _spm_package_impl(ctx): continue pkg_desc = pkg_descs_dict[pkg_name] clang_custom_infos_dict = pkg_clang_custom_infos_dict.get(pkg_name, default = {}) + product_refs = [pr for pr in declared_product_refs if refs.is_product_ref(pr, for_pkg = pkg_name)] pkg_build_infos_dict[pkg_name] = _gather_package_build_info( ctx, build_config_path, clang_custom_infos_dict, pkg_desc, + product_refs, target_refs, ) diff --git a/spm/internal/spm_package_info_utils.bzl b/spm/internal/spm_package_info_utils.bzl index e66ef37b..ef498f80 100644 --- a/spm/internal/spm_package_info_utils.bzl +++ b/spm/internal/spm_package_info_utils.bzl @@ -27,6 +27,9 @@ def _get_module_info(pkg_info, module_name): module a `struct` value as created by `providers.clang_module()` is returned. """ + for binary in pkg_info.swift_binaries: + if binary.name == module_name: + return binary for module in pkg_info.swift_modules: if module.module_name == module_name: return module diff --git a/spm/internal/spm_repositories.bzl b/spm/internal/spm_repositories.bzl index 5f9d8b80..25ce6d5c 100644 --- a/spm/internal/spm_repositories.bzl +++ b/spm/internal/spm_repositories.bzl @@ -74,7 +74,7 @@ def _find_and_delete_files(repository_ctx, path, name): _spm_swift_binary_tpl = """ spm_swift_binary( - name = "{module_name}", + name = "{exec_name}", packages = "@{repo_name}//:build", visibility = ["//visibility:public"], ) @@ -145,21 +145,20 @@ def _create_deps_str(pkg_name, target_deps): deps = [" \"%s\"," % (label) for label in target_labels] return "\n".join(deps) -def _create_spm_swift_binary_decl(repository_ctx, pkg_name, target): +def _create_spm_swift_binary_decl(repository_ctx, pkg_name, product): """Returns the spm_swift_library declaration for this Swift target. Args: repository_ctx: A `repository_ctx` instance. pkg_name: The name of the Swift package as a `string`. - target: A target `dict` from a package description JSON. + product: A product `dict` from a package description JSON. Returns: - A `string` representing an `spm_swift_library` declaration. + A `string` representing an `spm_swift_binary` declaration. """ - module_name = target["name"] return _spm_swift_binary_tpl.format( repo_name = repository_ctx.attr.name, - module_name = module_name, + exec_name = product["name"], ) def _create_spm_swift_library_decl(repository_ctx, pkg_name, target, target_deps): @@ -217,27 +216,43 @@ def _create_spm_system_library_decl(repository_ctx, pkg_name, target, target_dep deps_str = _create_deps_str(pkg_name, target_deps) return _spm_system_library_tpl % (module_name, repository_ctx.attr.name, deps_str) -def _generate_bazel_pkg(repository_ctx, pkg_desc, dep_target_refs_dict, clang_hdrs_dict, pkg_root_path): +def _generate_bazel_pkg( + repository_ctx, + pkg_desc, + dep_target_refs_dict, + clang_hdrs_dict, + exec_products, + pkg_root_path): """Generate a Bazel package for the specified Swift package. Args: repository_ctx: A `repository_ctx` instance. pkg_desc: A package description `dict`. - dep_target_refs_dict: A `dict` of target refs and their dependenceis. + dep_target_refs_dict: A `dict` of target refs and their dependencies. clang_hdrs_dict: A `dict` where the values are a `list` of clang public header path `string` values and the keys are a `string` created by `spm_common.create_clang_hdrs_key()`. + exec_products: A `list` of product `dict` from the package description + that are executable. pkg_root_path: A path `string` specifying the location of the package which defines the target. """ pkg_name = pkg_desc["name"] bld_path = "%s/BUILD.bazel" % (pkg_name) + module_decls = [] + + # Create a binary target for the executable products + for product in exec_products: + module_decls.append(_create_spm_swift_binary_decl( + repository_ctx, + pkg_name, + product, + )) + # Collect the target refs for the specified package target_refs = [tr for tr in dep_target_refs_dict if refs.is_target_ref(tr, for_pkg = pkg_name)] - - module_decls = [] for target_ref in target_refs: target_deps = dep_target_refs_dict[target_ref] rtype, pname, target_name = refs.split(target_ref) @@ -258,11 +273,9 @@ def _generate_bazel_pkg(repository_ctx, pkg_desc, dep_target_refs_dict, clang_hd target_deps, )) elif pds.is_executable_target(target): - module_decls.append(_create_spm_swift_binary_decl( - repository_ctx, - pkg_name, - target, - )) + # Do not generate a Bazel target for the executable. One will + # be created for the executable product. + pass else: fail("Unrecognized Swift target type. %s" % (target)) @@ -597,6 +610,16 @@ def _configure_spm_repository(repository_ctx, pkgs): # dependencies declared_product_refs = packages.get_product_refs(pkgs) + # Index the executable products by package name. + exec_products_dict = {} + for product_ref in declared_product_refs: + ref_type, pkg_name, product_name = refs.split(product_ref) + exec_products = exec_products_dict.setdefault(pkg_name, default = []) + product = pds.get_product(pkg_descs_dict[pkg_name], product_name) + if pds.is_executable_product(product): + exec_products.append(product) + exec_products_dict[pkg_name] = exec_products + dep_target_refs_dict = pds.transitive_dependencies(pkg_descs_dict, declared_product_refs) for pkg_name in pkg_descs_dict: _generate_bazel_pkg( @@ -604,6 +627,7 @@ def _configure_spm_repository(repository_ctx, pkgs): pkg_descs_dict[pkg_name], dep_target_refs_dict, clang_hdrs_dict, + exec_products_dict.get(pkg_name, default = []), pkg_root_path = paths.join(spm_common.checkouts_path, pkg_name), ) diff --git a/spm/internal/spm_swift_binary.bzl b/spm/internal/spm_swift_binary.bzl index 11975519..00d4b41e 100644 --- a/spm/internal/spm_swift_binary.bzl +++ b/spm/internal/spm_swift_binary.bzl @@ -25,17 +25,17 @@ def _spm_swift_binary_impl(ctx): module_name = ctx.attr.name pkg_info = spm_package_info_utils.get(pkgs_info.packages, pkg_name) - module_info = spm_package_info_utils.get_module_info(pkg_info, module_name) + binary_info = spm_package_info_utils.get_module_info(pkg_info, module_name) - if module_info.executable == None: + if binary_info.executable == None: fail("The specified module (%s) is not executable." % (module_name)) # Bazel will error if we try to return a file that we did not create as # the executable. So, we symlink it. - executable = ctx.actions.declare_file(module_info.executable.basename) + executable = ctx.actions.declare_file(binary_info.executable.basename) ctx.actions.symlink( output = executable, - target_file = module_info.executable, + target_file = binary_info.executable, ) return [ diff --git a/test/package_descriptions_tests.bzl b/test/package_descriptions_tests.bzl index 3b20b1f0..9a7cff6c 100644 --- a/test/package_descriptions_tests.bzl +++ b/test/package_descriptions_tests.bzl @@ -17,6 +17,38 @@ def _parse_json_test(ctx): parse_json_test = unittest.make(_parse_json_test) +def _get_product_test(ctx): + env = unittest.begin(ctx) + + pkg_desc = { + "products": [ + {"name": "Foo", "type": {"library": {}}}, + {"name": "Chicken", "type": {"executable": None}}, + {"name": "Bar", "type": {"library": {}}}, + ], + } + result = pds.get_product(pkg_desc, "Foo") + asserts.equals(env, "Foo", result["name"]) + + result = pds.get_product(pkg_desc, "Chicken") + asserts.equals(env, "Chicken", result["name"]) + + return unittest.end(env) + +get_product_test = unittest.make(_get_product_test) + +def _is_executable_product_test(ctx): + env = unittest.begin(ctx) + + product = {"type": {"library": {}}} + asserts.false(env, pds.is_executable_product(product)) + product = {"type": {"executable": None}} + asserts.true(env, pds.is_executable_product(product)) + + return unittest.end(env) + +is_executable_product_test = unittest.make(_is_executable_product_test) + def _is_library_product_test(ctx): env = unittest.begin(ctx) @@ -291,6 +323,8 @@ def package_descriptions_test_suite(): unittest.suite( "package_description_tests", parse_json_test, + get_product_test, + is_executable_product_test, is_library_product_test, library_products_test, is_library_target_test, diff --git a/test/providers_tests.bzl b/test/providers_tests.bzl index 06b0bd00..944679f6 100644 --- a/test/providers_tests.bzl +++ b/test/providers_tests.bzl @@ -1,6 +1,25 @@ load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest") load("//spm/internal:providers.bzl", "providers") +def _swift_binary_test(ctx): + env = unittest.begin(ctx) + + result = providers.swift_binary( + "MyBinary", + executable = "exec", + all_outputs = ["all"], + ) + expected = struct( + name = "MyBinary", + executable = "exec", + all_outputs = ["all"], + ) + asserts.equals(env, expected, result) + + return unittest.end(env) + +swift_binary_test = unittest.make(_swift_binary_test) + def _swift_module_test(ctx): env = unittest.begin(ctx) @@ -103,6 +122,7 @@ system_library_module_test = unittest.make(_system_library_module_test) def providers_test_suite(): return unittest.suite( "providers_tests", + swift_binary_test, swift_module_test, clang_module_test, copy_info_test, diff --git a/test/references_tests.bzl b/test/references_tests.bzl index e113a64c..95098e92 100644 --- a/test/references_tests.bzl +++ b/test/references_tests.bzl @@ -67,6 +67,25 @@ def _is_target_ref_test(ctx): is_target_ref_test = unittest.make(_is_target_ref_test) +def _is_product_ref_test(ctx): + env = unittest.begin(ctx) + + ref = references.create(reference_types.product, "foo-kit", "FooKit") + asserts.true(env, references.is_product_ref(ref)) + + ref = references.create(reference_types.target, "foo-kit", "FooKit") + asserts.false(env, references.is_product_ref(ref)) + + ref = references.create(reference_types.product, "foo-kit", "FooKit") + asserts.true(env, references.is_product_ref(ref, for_pkg = "foo-kit")) + + ref = references.create(reference_types.product, "foo-kit", "FooKit") + asserts.false(env, references.is_product_ref(ref, for_pkg = "bar-kit")) + + return unittest.end(env) + +is_product_ref_test = unittest.make(_is_product_ref_test) + def references_test_suite(): return unittest.suite( "references_tests", @@ -75,4 +94,5 @@ def references_test_suite(): create_product_ref_test, create_target_ref_test, is_target_ref_test, + is_product_ref_test, ) diff --git a/test/spm_package_info_utils_tests.bzl b/test/spm_package_info_utils_tests.bzl index 73c6d90c..bf5b4f22 100644 --- a/test/spm_package_info_utils_tests.bzl +++ b/test/spm_package_info_utils_tests.bzl @@ -8,12 +8,14 @@ def _get_test(ctx): pkg_infos = [ SPMPackageInfo( name = "hello", + swift_binaries = [], swift_modules = [], clang_modules = [], system_library_modules = [], ), SPMPackageInfo( name = "goodbye", + swift_binaries = [], swift_modules = [], clang_modules = [], system_library_modules = [], @@ -33,6 +35,10 @@ def _get_module_info_test(ctx): pkg_info = SPMPackageInfo( name = "hello", + swift_binaries = [ + providers.swift_binary("MySwiftBinary"), + providers.swift_binary("NotMySwiftBinary"), + ], swift_modules = [ providers.swift_module("MySwift"), providers.swift_module("NotMySwift"), @@ -47,6 +53,9 @@ def _get_module_info_test(ctx): ], ) + result = spm_package_info_utils.get_module_info(pkg_info, "MySwiftBinary") + asserts.equals(env, "MySwiftBinary", result.name) + result = spm_package_info_utils.get_module_info(pkg_info, "MySwift") asserts.equals(env, "MySwift", result.module_name)