From 880db7fa87e813b1837b8a214d8cff6dd560fff6 Mon Sep 17 00:00:00 2001 From: Brentley Jones Date: Fri, 27 Sep 2024 08:28:03 -0500 Subject: [PATCH] feat: set license information for packages with rules_license (#1118) Resolves #1092. Signed-off-by: Brentley Jones --- MODULE.bazel | 1 + deps.bzl | 10 ++ docs/repository_rules_overview.md | 3 +- swiftpkg/bzlmod/swift_deps.bzl | 1 + swiftpkg/internal/build_decls.bzl | 5 +- swiftpkg/internal/build_files.bzl | 15 ++- swiftpkg/internal/pkginfos.bzl | 22 ++++- swiftpkg/internal/repo_rules.bzl | 13 ++- swiftpkg/internal/repository_files.bzl | 44 +++++++++ swiftpkg/internal/starlark_codegen.bzl | 2 +- swiftpkg/internal/swift_package.bzl | 1 + swiftpkg/internal/swiftpkg_build_files.bzl | 57 ++++++++++++ swiftpkg/tests/swiftpkg_build_files_tests.bzl | 93 +++++++++++++++++++ 13 files changed, 261 insertions(+), 6 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 2c2c3fe1b..8a0387aba 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -7,6 +7,7 @@ module( bazel_dep(name = "cgrindel_bazel_starlib", version = "0.21.0") bazel_dep(name = "bazel_skylib", version = "1.4.2") +bazel_dep(name = "rules_license", version = "0.0.8") bazel_dep( name = "rules_go", version = "0.47.0", diff --git a/deps.bzl b/deps.bzl index 61a145fa2..b2a1a8cb2 100644 --- a/deps.bzl +++ b/deps.bzl @@ -44,3 +44,13 @@ def swift_bazel_dependencies(): "https://github.com/cgrindel/bazel-starlib/releases/download/v0.21.0/bazel-starlib.v0.21.0.tar.gz", ], ) + + maybe( + http_archive, + name = "rules_license", + sha256 = "241b06f3097fd186ff468832150d6cc142247dc42a32aaefb56d0099895fd229", + urls = [ + "https://mirror.bazel.build/github.com/bazelbuild/rules_license/releases/download/0.0.8/rules_license-0.0.8.tar.gz", + "https://github.com/bazelbuild/rules_license/releases/download/0.0.8/rules_license-0.0.8.tar.gz", + ], + ) diff --git a/docs/repository_rules_overview.md b/docs/repository_rules_overview.md index 1e6b52ef1..a69fb31c0 100755 --- a/docs/repository_rules_overview.md +++ b/docs/repository_rules_overview.md @@ -42,7 +42,7 @@ Used to build a local Swift package.
 swift_package(name, bazel_package_name, branch, commit, dependencies_index, env, init_submodules,
               patch_args, patch_cmds, patch_cmds_win, patch_tool, patches, recursive_init_submodules,
-              remote, repo_mapping, shallow_since, tag, verbose)
+              remote, repo_mapping, shallow_since, tag, verbose, version)
 
Used to download and build an external Swift package. @@ -70,5 +70,6 @@ Used to download and build an external Swift package. | shallow_since | an optional date, not after the specified commit; the argument is not allowed if a tag is specified (which allows cloning with depth 1). Setting such a date close to the specified commit allows for a more shallow clone of the repository, saving bandwidth and wall-clock time. | String | optional | `""` | | tag | tag in the remote repository to checked out. Precisely one of branch, tag, or commit must be specified. | String | optional | `""` | | verbose | - | Boolean | optional | `False` | +| version | The resolved version of the package. | String | optional | `""` | diff --git a/swiftpkg/bzlmod/swift_deps.bzl b/swiftpkg/bzlmod/swift_deps.bzl index 064fe0633..b852abc26 100644 --- a/swiftpkg/bzlmod/swift_deps.bzl +++ b/swiftpkg/bzlmod/swift_deps.bzl @@ -164,6 +164,7 @@ def _declare_pkg_from_dependency(dep, config_pkg): bazel_package_name = name, commit = pin.state.revision, remote = pin.location, + version = pin.state.version, dependencies_index = None, init_submodules = init_submodules, recursive_init_submodules = recursive_init_submodules, diff --git a/swiftpkg/internal/build_decls.bzl b/swiftpkg/internal/build_decls.bzl index 23bc79b39..ca9d9cb95 100644 --- a/swiftpkg/internal/build_decls.bzl +++ b/swiftpkg/internal/build_decls.bzl @@ -29,7 +29,10 @@ def _to_starlark_parts(decl, indent): for c in decl.comments: parts.append(scg.indent(indent, "{}\n".format(c))) parts.append(scg.indent(indent, "{}(\n".format(decl.kind))) - parts.extend(scg.new_attr("name", decl.name, indent + 1)) + + # Name won't be set for `package` declarations + if decl.name: + parts.extend(scg.new_attr("name", decl.name, indent + 1)) # Sort the keys to ensure that we have a consistent output. It would be # ideal to output them in a manner that matches Buildifier output rules. diff --git a/swiftpkg/internal/build_files.bzl b/swiftpkg/internal/build_files.bzl index 9c2a08bd3..ec9566c82 100644 --- a/swiftpkg/internal/build_files.bzl +++ b/swiftpkg/internal/build_files.bzl @@ -5,12 +5,14 @@ load(":build_decls.bzl", "build_decls") load(":load_statements.bzl", "load_statements") load(":starlark_codegen.bzl", scg = "starlark_codegen") -def _new(load_stmts = [], decls = []): +def _new(load_stmts = [], package_attrs = {}, decls = []): """Create a `struct` that represents the parts of a Bazel build file. Args: load_stmts: A `list` of load statement `struct` values as returned by `load_statements.new`. + package_attrs: A `dict` of attributes to set on the `package` + declaration. decls: A `list` of declaration `struct` values as returned by `build_decls.new`. @@ -19,6 +21,7 @@ def _new(load_stmts = [], decls = []): """ return struct( load_stmts = load_stmts, + package_attrs = package_attrs, decls = decls, to_starlark_parts = _to_starlark_parts, ) @@ -27,6 +30,13 @@ def _to_starlark_parts(build_file, indent): parts = [] for load_stmt in build_file.load_stmts: parts.extend([scg.with_indent(indent, load_stmt), "\n"]) + if build_file.package_attrs: + package_decl = build_decls.new( + "package", + None, + attrs = build_file.package_attrs, + ) + parts.extend(["\n", scg.with_indent(indent, package_decl), "\n"]) for decl in build_file.decls: parts.extend(["\n", scg.with_indent(indent, decl), "\n"]) return parts @@ -48,14 +58,17 @@ def _merge(*bld_files): fail("Attempted to merge build files, but none were provided.") load_stmts = [] + package_attrs = {} decls = [] for bf in bld_files: load_stmts.extend(bf.load_stmts) + package_attrs |= bf.package_attrs decls.extend(bf.decls) load_stmts = load_statements.uniq(load_stmts) decls = build_decls.uniq(decls) return _new( load_stmts = load_stmts, + package_attrs = package_attrs, decls = decls, ) diff --git a/swiftpkg/internal/pkginfos.bzl b/swiftpkg/internal/pkginfos.bzl index 6fa2d079c..6fa956e00 100644 --- a/swiftpkg/internal/pkginfos.bzl +++ b/swiftpkg/internal/pkginfos.bzl @@ -598,6 +598,18 @@ def _new_from_parsed_json( ) targets.append(target) + url = None + version = None + if hasattr(repository_ctx, "attr"): + # We only want to try to collect url and version when called from + # `swift_package` + url = getattr(repository_ctx.attr, "remote", None) + version = getattr( + repository_ctx.attr, + "version", + getattr(repository_ctx.attr, "commit", None), + ) + return _new( name = dump_manifest["name"], path = pkg_path, @@ -610,6 +622,8 @@ def _new_from_parsed_json( dependencies = dependencies, products = products, targets = targets, + url = url, + version = version, ) # MARK: - Swift Package @@ -622,7 +636,9 @@ def _new( platforms = [], dependencies = [], products = [], - targets = []): + targets = [], + url = None, + version = None): """Returns a `struct` representing information about a Swift package. Args: @@ -639,6 +655,8 @@ def _new( `pkginfos.new_product()`. targets: A `list` of target structs as created by `pkginfos.new_target()`. + url: Optional. The url of the package (`string`). + version: Optional. The semantic version of the package (`string`). Returns: A `struct` representing information about a Swift package. @@ -652,6 +670,8 @@ def _new( dependencies = dependencies, products = products, targets = targets, + url = url, + version = version, ) # MARK: - Platform diff --git a/swiftpkg/internal/repo_rules.bzl b/swiftpkg/internal/repo_rules.bzl index dd4fd9f14..3565e9e80 100644 --- a/swiftpkg/internal/repo_rules.bzl +++ b/swiftpkg/internal/repo_rules.bzl @@ -65,8 +65,19 @@ higher. Found version %s installed.\ def _gen_build_files(repository_ctx, pkg_ctx): pkg_info = pkg_ctx.pkg_info - # Create Bazel declarations for the Swift package targets bld_files = [] + + licenses = repository_files.find_license_files(repository_ctx) + bld_files.append( + # Pick the shortest name, in order to prefer `LICENSE` over + # `LICENSE.md` + swiftpkg_build_files.new_for_license( + pkg_info, + sorted(licenses, key = len)[0] if licenses else None, + ), + ) + + # Create Bazel declarations for the Swift package targets for target in pkg_info.targets: # Unfortunately, Package.resolved does not contain test-only external # dependencies. So, we need to skip generating test targets. diff --git a/swiftpkg/internal/repository_files.bzl b/swiftpkg/internal/repository_files.bzl index a907f83bd..e595500ea 100644 --- a/swiftpkg/internal/repository_files.bzl +++ b/swiftpkg/internal/repository_files.bzl @@ -18,6 +18,49 @@ def _path_exists(repository_ctx, path): ) return exec_result.return_code == 0 +def _find_license_files(repository_ctx): + """Retrieves all license files at the root of the package. + + Args: + repository_ctx: A `repository_ctx` instance. + + Returns: + A `list` of path `string` values. + """ + + find_args = [ + "find", + # Follow symlinks and report on the actual files. + "-H", + "-L", + ".", + # For GNU find, it is important for the global options (e.g. -maxdepth) + # to be specified BEFORE other options like -type. Also, GNU find does + # not support -depth . So, we approximate it by using -mindepth + # and -maxdepth. + "-mindepth", + "1", + "-maxdepth", + "1", + "-type", + "f", + "(", + "-name", + "LICENSE", + "-o", + "-name", + "LICENSE.*", + ")", + ] + + exec_result = repository_ctx.execute(find_args, quiet = True) + if exec_result.return_code != 0: + fail("Failed to find license files. stderr:\n%s" % exec_result.stderr) + return _process_find_results( + exec_result.stdout, + find_path = ".", + ) + def _list_files_under( repository_ctx, path, @@ -247,6 +290,7 @@ repository_files = struct( exclude_paths = _exclude_paths, file_type = _file_type, find_and_delete_files = _find_and_delete_files, + find_license_files = _find_license_files, is_directory = _is_directory, list_directories_under = _list_directories_under, list_files_under = _list_files_under, diff --git a/swiftpkg/internal/starlark_codegen.bzl b/swiftpkg/internal/starlark_codegen.bzl index 55a05b008..d11d37f0a 100644 --- a/swiftpkg/internal/starlark_codegen.bzl +++ b/swiftpkg/internal/starlark_codegen.bzl @@ -5,7 +5,7 @@ _single_indent_str = " " # MARK: - Simple Type Detection _simple_starlark_types = [ - "None", + "NoneType", "bool", "int", "string", diff --git a/swiftpkg/internal/swift_package.bzl b/swiftpkg/internal/swift_package.bzl index 5dcd11fd7..fb21b87af 100644 --- a/swiftpkg/internal/swift_package.bzl +++ b/swiftpkg/internal/swift_package.bzl @@ -180,6 +180,7 @@ _ALL_ATTRS = dicts.add( _GIT_ATTRS, repo_rules.env_attrs, repo_rules.swift_attrs, + {"version": attr.string(doc = "The resolved version of the package.")}, ) swift_package = repository_rule( diff --git a/swiftpkg/internal/swiftpkg_build_files.bzl b/swiftpkg/internal/swiftpkg_build_files.bzl index c8bbf1ac2..f87f3d31f 100644 --- a/swiftpkg/internal/swiftpkg_build_files.bzl +++ b/swiftpkg/internal/swiftpkg_build_files.bzl @@ -835,6 +835,44 @@ Expected only one target for the macro product {name} but received {count}.\ ], ) +# MARK: - License + +def _new_for_license(pkg_info, license): + packageinfo_target_name = "package_info.rspm" + decls = [ + build_decls.new( + rules_license_kinds.package_info, + packageinfo_target_name, + attrs = { + "package_name": pkg_info.name, + "package_url": pkg_info.url, + "package_version": pkg_info.version, + }, + ), + ] + default_package_metadata = [":{}".format(packageinfo_target_name)] + load_stmts = [rules_license_package_info_load_stmt] + + if license: + license_target_name = "license.rspm" + decls.append( + build_decls.new( + rules_license_kinds.license, + license_target_name, + attrs = { + "license_text": license, + }, + ), + ) + load_stmts.append(rules_license_license_load_stmt) + default_package_metadata.insert(0, ":{}".format(license_target_name)) + + return build_files.new( + load_stmts = load_stmts, + package_attrs = {"default_package_metadata": default_package_metadata}, + decls = decls, + ) + # MARK: - Constants and API Definition swift_location = "@build_bazel_rules_swift//swift:swift.bzl" @@ -893,6 +931,24 @@ native_kinds = struct( alias = "alias", ) +rules_license_license_location = "@rules_license//rules:license.bzl" +rules_license_package_info_location = "@rules_license//rules:package_info.bzl" + +rules_license_kinds = struct( + license = "license", + package_info = "package_info", +) + +rules_license_license_load_stmt = load_statements.new( + rules_license_license_location, + rules_license_kinds.license, +) + +rules_license_package_info_load_stmt = load_statements.new( + rules_license_package_info_location, + rules_license_kinds.package_info, +) + skylib_build_test_location = "@bazel_skylib//rules:build_test.bzl" skylib_kinds = struct( @@ -905,6 +961,7 @@ skylib_build_test_load_stmt = load_statements.new( ) swiftpkg_build_files = struct( + new_for_license = _new_for_license, new_for_target = _new_for_target, new_for_products = _new_for_products, new_for_product = _new_for_product, diff --git a/swiftpkg/tests/swiftpkg_build_files_tests.bzl b/swiftpkg/tests/swiftpkg_build_files_tests.bzl index 7a8ec9e50..cd4b23b4f 100644 --- a/swiftpkg/tests/swiftpkg_build_files_tests.bzl +++ b/swiftpkg/tests/swiftpkg_build_files_tests.bzl @@ -34,6 +34,8 @@ _pkg_info = pkginfos.new( name = "MyPackage", path = "/path/to/my-package", tools_version = "5.9", + url = "https://github.com/my/package", + version = "0.4.2", dependencies = [ pkginfos.new_dependency( identity = "swift-argument-parser", @@ -1044,9 +1046,100 @@ alias( product_generation_test = unittest.make(_product_generation_test) +def _license_generation_test(ctx): + env = unittest.begin(ctx) + + tests = [ + struct( + msg = "No license", + license = None, + exp = """\ +load("@rules_license//rules:package_info.bzl", "package_info") + +package( + default_package_metadata = [":package_info.rspm"], +) + +package_info( + name = "package_info.rspm", + package_name = "MyPackage", + package_url = "https://github.com/my/package", + package_version = "0.4.2", +) +""", + ), + struct( + msg = "Markdown license", + license = "LICENSE.md", + exp = """\ +load("@rules_license//rules:package_info.bzl", "package_info") +load("@rules_license//rules:license.bzl", "license") + +package( + default_package_metadata = [ + ":license.rspm", + ":package_info.rspm", + ], +) + +package_info( + name = "package_info.rspm", + package_name = "MyPackage", + package_url = "https://github.com/my/package", + package_version = "0.4.2", +) + +license( + name = "license.rspm", + license_text = "LICENSE.md", +) +""", + ), + struct( + msg = "License", + license = "LICENSE", + exp = """\ +load("@rules_license//rules:package_info.bzl", "package_info") +load("@rules_license//rules:license.bzl", "license") + +package( + default_package_metadata = [ + ":license.rspm", + ":package_info.rspm", + ], +) + +package_info( + name = "package_info.rspm", + package_name = "MyPackage", + package_url = "https://github.com/my/package", + package_version = "0.4.2", +) + +license( + name = "license.rspm", + license_text = "LICENSE", +) +""", + ), + ] + for t in tests: + actual = scg.to_starlark( + swiftpkg_build_files.new_for_license( + pkg_info = _pkg_info, + license = t.license, + ), + ) + asserts.equals(env, t.exp, actual, t.msg) + + return unittest.end(env) + +license_generation_test = unittest.make(_license_generation_test) + def swiftpkg_build_files_test_suite(): return unittest.suite( "swiftpkg_build_files_tests", target_generation_test, product_generation_test, + license_generation_test, )