Make our rules output `PackageInfo` where possible (#1232)
diff --git a/BUILD b/BUILD index 8ed0a3c..9bb85b6 100644 --- a/BUILD +++ b/BUILD
@@ -12,6 +12,7 @@ srcs = [ ":defs.bzl", ":specs.bzl", + "@rules_license//:docs_deps", ], visibility = [ # This library is only visible to allow others who depend on
diff --git a/MODULE.bazel b/MODULE.bazel index 79499cb..03e14fe 100644 --- a/MODULE.bazel +++ b/MODULE.bazel
@@ -5,6 +5,10 @@ ) bazel_dep( + name = "rules_android", + version = "0.1.1", +) +bazel_dep( name = "bazel_features", version = "1.15.0", ) @@ -17,6 +21,10 @@ version = "0.0.10", ) bazel_dep( + name = "rules_license", + version = "1.0.0", +) +bazel_dep( name = "rules_java", version = "7.10.0", ) @@ -25,11 +33,6 @@ version = "1.9.6", ) bazel_dep( - name = "rules_android", - version = "0.1.1", -) - -bazel_dep( name = "stardoc", version = "0.7.0", dev_dependency = True, @@ -309,7 +312,12 @@ name = "jvm_import_test", artifacts = [ "com.google.code.findbugs:jsr305:3.0.2", + "com.android.support:appcompat-v7:aar:28.0.0", ], + repositories = [ + "https://repo1.maven.org/maven2", + "https://maven.google.com", + ] ) dev_maven.install( name = "m2local_testing",
diff --git a/WORKSPACE b/WORKSPACE index a49e228..395db37 100644 --- a/WORKSPACE +++ b/WORKSPACE
@@ -52,6 +52,13 @@ stardoc_repositories() +http_archive( + name = "rules_testing", + sha256 = "02c62574631876a4e3b02a1820cb51167bb9cdcdea2381b2fa9d9b8b11c407c4", + strip_prefix = "rules_testing-0.6.0", + url = "https://github.com/bazelbuild/rules_testing/releases/download/v0.6.0/rules_testing-v0.6.0.tar.gz", +) + # Stardoc also depends on skydoc_repositories, rules_sass, rules_nodejs, but our # usage of Stardoc (scripts/generate_docs) doesn't require any of these # dependencies. So, we omit them to keep the WORKSPACE file simpler.
diff --git a/private/dependency_tree_parser.bzl b/private/dependency_tree_parser.bzl index b7de59a..d778bab 100644 --- a/private/dependency_tree_parser.bzl +++ b/private/dependency_tree_parser.bzl
@@ -28,6 +28,7 @@ "strip_packaging_and_classifier", "strip_packaging_and_classifier_and_version", ) +load("//private/lib:coordinates.bzl", "unpack_coordinates") def _genrule_copy_artifact_from_http_file(artifact, visibilities): http_file_repository = escape(artifact["coordinates"]) @@ -121,7 +122,7 @@ # is_dylib = False if packaging == "jar": - target_import_string.append("\tjars = [\"%s\"]," % artifact_path) + target_import_string.append("\tjar = \"%s\"," % artifact_path) if srcjar_paths != None and target_label in srcjar_paths: target_import_string.append("\tsrcjar = \"%s\"," % srcjar_paths[target_label]) elif packaging == "aar": @@ -220,6 +221,30 @@ target_import_string.append("\t\t\"maven_sha256=%s\"," % artifact["sha256"]) target_import_string.append("\t],") + if packaging == "jar": + target_import_string.append("\tmaven_coordinates = \"%s\"," % coordinates) + if len(artifact["urls"]): + target_import_string.append("\tmaven_url = \"%s\"," % artifact["urls"][0]) + else: + unpacked = unpack_coordinates(coordinates) + url = artifact["urls"][0] if len(artifact["urls"]) else None + + package_info_name = "%s_package_info" % target_label + target_import_string.append("\tapplicable_licenses = [\":%s\"]," % package_info_name) + to_return.append(""" +package_info( + name = {name}, + package_name = {coordinates}, + package_url = {url}, + package_version = {version}, +) +""".format( + coordinates = repr(coordinates), + name = repr(package_info_name), + url = repr(url), + version = repr(unpacked.version), + )) + # 6. If `neverlink` is True in the artifact spec, add the neverlink attribute to make this artifact # available only as a compile time dependency. #
diff --git a/private/lib/coordinates.bzl b/private/lib/coordinates.bzl new file mode 100644 index 0000000..f9772bd --- /dev/null +++ b/private/lib/coordinates.bzl
@@ -0,0 +1,133 @@ +def unpack_coordinates(coords): + """Takes a maven coordinate and unpacks it into a struct with fields + `groupId`, `artifactId`, `version`, `type`, `scope` + where type and scope are optional. + + Assumes `coords` is in one of the following syntaxes: + * groupId:artifactId[:type[:scope]]:version + * groupId:artifactId[:version][@classifier][:type] + """ + if not coords: + return None + + parts = coords.split(":") + nparts = len(parts) + + if nparts < 2: + fail("Unparsed: %s" % coords) + + # Both formats look the same for just `group:artifact` + if nparts == 2: + return struct( + groupId = parts[0], + artifactId = parts[1], + type = None, + scope = None, + version = None, + classifier = None, + ) + + # From here, we can be sure we have at least three `parts` + if _is_version_number(parts[2]): + return _unpack_gradle_format(coords) + + return _unpack_rje_format(coords, parts) + +def _is_version_number(part): + return part[0].isdigit() + +def _unpack_rje_format(coords, parts): + nparts = len(parts) + + if nparts < 3 or nparts > 5: + fail("Unparsed: %s" % coords) + + version = parts[-1] + parts = dict(enumerate(parts[:-1])) + return struct( + groupId = parts.get(0), + artifactId = parts.get(1), + type = parts.get(2), + scope = parts.get(3), + classifier = None, + version = version, + ) + +def _unpack_gradle_format(coords): + idx = coords.find("@") + type = None + if idx != -1: + type = coords[idx + 1:] + coords = coords[0:idx] + + parts = coords.split(":") + nparts = len(parts) + + if nparts < 3 or nparts > 4: + fail("Unparsed: %s" % coords) + + parts = dict(enumerate(parts)) + + return struct( + groupId = parts.get(0), + artifactId = parts.get(1), + version = parts.get(2), + classifier = parts.get(3), + type = type, + scope = None, + ) + +def to_external_form(coords): + """Formats `coords` as a string suitable for use by tools such as Gradle. + + The returned format matches Gradle's "external dependency" short-form + syntax: `group:name:version:classifier@type` + """ + + if type(coords) == "string": + unpacked = unpack_coordinates(coords) + else: + unpacked = coords + + to_return = "%s:%s:%s" % (unpacked.groupId, unpacked.artifactId, unpacked.version) + + if hasattr(unpacked, "classifier"): + if unpacked.classifier and unpacked.classifier != "jar": + to_return += ":" + unpacked.classifier + + if hasattr(unpacked, "type"): + if unpacked.type and unpacked.type != "jar": + to_return += "@" + unpacked.type + + return to_return + +_DEFAULT_PURL_REPOS = [ + "https://repo.maven.apache.org/maven2", + "https://repo.maven.apache.org/maven2/", + "https://repo1.maven.org", + "https://repo1.maven.org/", +] + +def to_purl(coords, repository): + to_return = "pkg:maven/" + + unpacked = unpack_coordinates(coords) + to_return += "{group}:{artifact}@{version}".format( + artifact = unpacked.artifactId, + group = unpacked.groupId, + version = unpacked.version, + ) + + suffix = [] + if unpacked.classifier: + suffix.append("classifier=" + unpacked.classifier) + if unpacked.type: + suffix.append("type=" + unpacked.type) + if repository and repository not in _DEFAULT_PURL_REPOS: + # Default repository name is pulled from https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst + suffix.append("repository=" + repository) + + if len(suffix): + to_return += "?" + "&".join(suffix) + + return to_return
diff --git a/private/lib/urls.bzl b/private/lib/urls.bzl index 9200027..5f6aa7e 100644 --- a/private/lib/urls.bzl +++ b/private/lib/urls.bzl
@@ -17,6 +17,20 @@ url_parts = url_without_protocol.split("/") return protocol, url_parts +_REMOTE_SCHEMES = [ + "ftp", + "http", + "https", +] + +def scheme_and_host(url): + if not url: + return None + + new_url = remove_auth_from_url(url) + (protocol, url_parts) = split_url(new_url) + return protocol + "://" + url_parts[0] + def remove_auth_from_url(url): """Returns url without `user:pass@` or `user@`.""" if "@" not in url:
diff --git a/private/rules/coursier.bzl b/private/rules/coursier.bzl index 6ef641f..d5fec29 100644 --- a/private/rules/coursier.bzl +++ b/private/rules/coursier.bzl
@@ -37,6 +37,7 @@ # package(default_visibility = [{visibilities}]) # https://github.com/bazelbuild/bazel/issues/13681 load("@bazel_skylib//:bzl_library.bzl", "bzl_library") +load("@rules_license//rules:package_info.bzl", "package_info") load("@rules_jvm_external//private/rules:pin_dependencies.bzl", "pin_dependencies") load("@rules_jvm_external//private/rules:jvm_import.bzl", "jvm_import") {aar_import_statement}
diff --git a/private/rules/java_export.bzl b/private/rules/java_export.bzl index 09283f1..80ef633 100644 --- a/private/rules/java_export.bzl +++ b/private/rules/java_export.bzl
@@ -216,6 +216,7 @@ maven_project_jar( name = "%s-project" % name, target = ":%s" % lib_name, + maven_coordinates = maven_coordinates, manifest_entries = manifest_entries, deploy_env = deploy_env, excluded_workspaces = excluded_workspaces.keys(),
diff --git a/private/rules/jvm_import.bzl b/private/rules/jvm_import.bzl index 2fc1eee..0fbbd10 100644 --- a/private/rules/jvm_import.bzl +++ b/private/rules/jvm_import.bzl
@@ -8,8 +8,10 @@ # [1]: https://github.com/bazelbuild/bazel/issues/4549 load("@rules_java//java:defs.bzl", "JavaInfo") +load("@rules_license//rules:providers.bzl", "PackageInfo") +load("//private/lib:coordinates.bzl", "to_external_form", "to_purl", "unpack_coordinates") +load("//private/lib:urls.bzl", "scheme_and_host") load("//settings:stamp_manifest.bzl", "StampManifestProvider") -load(":maven_utils.bzl", "unpack_coordinates") def _jvm_import_impl(ctx): if not ctx.attr.jar and not ctx.attr.jars: @@ -66,6 +68,21 @@ progress_message = "Creating compile jar for %s" % ctx.label, ) + additional_providers = [] + if ctx.attr.maven_coordinates: + unpacked = unpack_coordinates(ctx.attr.maven_coordinates) + + additional_providers.append( + PackageInfo( + type = "jvm_import", + label = ctx.label, + package_url = ctx.attr.maven_url, + package_version = unpacked.version, + package_name = to_external_form(ctx.attr.maven_coordinates), + purl = to_purl(ctx.attr.maven_coordinates, scheme_and_host(ctx.attr.maven_url)), + ), + ) + return [ DefaultInfo( files = depset([outjar]), @@ -81,7 +98,7 @@ ], neverlink = ctx.attr.neverlink, ), - ] + ] + additional_providers jvm_import = rule( attrs = { @@ -105,6 +122,12 @@ "neverlink": attr.bool( default = False, ), + "maven_coordinates": attr.string( + doc = "The maven coordinates that the `jar` can be downloaded from.", + ), + "maven_url": attr.string( + doc = "URL from where `jar` will be downloaded from.", + ), "_add_jar_manifest_entry": attr.label( executable = True, cfg = "exec",
diff --git a/private/rules/maven_bom.bzl b/private/rules/maven_bom.bzl index a6ae9b8..906c68c 100644 --- a/private/rules/maven_bom.bzl +++ b/private/rules/maven_bom.bzl
@@ -1,6 +1,7 @@ +load("//private/lib:coordinates.bzl", "unpack_coordinates") load(":maven_bom_fragment.bzl", "MavenBomFragmentInfo") load(":maven_publish.bzl", "maven_publish") -load(":maven_utils.bzl", "generate_pom", "unpack_coordinates") +load(":maven_utils.bzl", "generate_pom") _NON_EXISTENT_LABEL = Label("//:thisdoesnotexistinrulesjvmexternal")
diff --git a/private/rules/maven_project_jar.bzl b/private/rules/maven_project_jar.bzl index 9753283..8637156 100644 --- a/private/rules/maven_project_jar.bzl +++ b/private/rules/maven_project_jar.bzl
@@ -1,5 +1,7 @@ load("@rules_java//java:defs.bzl", "JavaInfo", "java_common") +load("@rules_license//rules:providers.bzl", "PackageInfo") load("//private/lib:bzlmod.bzl", "get_module_name_of_owner_of_repo") +load("//private/lib:coordinates.bzl", "to_external_form", "to_purl", "unpack_coordinates") load(":has_maven_deps.bzl", "MavenInfo", "calculate_artifact_jars", "calculate_artifact_source_jars", "has_maven_deps") load(":maven_utils.bzl", "determine_additional_dependencies") @@ -148,6 +150,21 @@ exports = exported_infos, ) + package_info = [] + if ctx.attr.maven_coordinates: + unpacked = unpack_coordinates(ctx.attr.maven_coordinates) + + package_info.append( + PackageInfo( + type = "java_export", + label = ctx.label, + package_url = None, + package_name = to_external_form(ctx.attr.maven_coordinates), + package_version = unpacked.version, + purl = to_purl(ctx.attr.maven_coordinates, None), + ), + ) + return [ DefaultInfo( files = depset([bin_jar]), @@ -163,7 +180,7 @@ _source_jars = [src_jar], ), java_info, - ] + ] + package_info maven_project_jar = rule( _maven_project_jar_impl, @@ -186,6 +203,9 @@ has_maven_deps, ], ), + "maven_coordinates": attr.string( + doc = "Coordinates that this artifact will be published from", + ), "manifest_entries": attr.string_dict( doc = "A dict of `String: String` containing additional manifest entry attributes and values.", ),
diff --git a/private/rules/maven_utils.bzl b/private/rules/maven_utils.bzl index ac46e81..08bdb54 100644 --- a/private/rules/maven_utils.bzl +++ b/private/rules/maven_utils.bzl
@@ -1,40 +1,9 @@ load("//private/lib:bzlmod.bzl", "get_module_name_of_owner_of_repo") +load("//private/lib:coordinates.bzl", _unpack_coordinates = "unpack_coordinates") def unpack_coordinates(coords): - """Takes a maven coordinate and unpacks it into a struct with fields - `groupId`, `artifactId`, `version`, `type`, `scope` - where type and scope are optional. - - Assumes following maven coordinate syntax: - groupId:artifactId[:type[:scope]]:version - """ - if not coords: - return None - - parts = coords.split(":") - nparts = len(parts) - - if nparts == 2: - return struct( - groupId = parts[0], - artifactId = parts[1], - type = None, - scope = None, - version = None, - ) - - if nparts < 3 or nparts > 5: - fail("Unparsed: %s" % coords) - - version = parts[-1] - parts = dict(enumerate(parts[:-1])) - return struct( - groupId = parts.get(0), - artifactId = parts.get(1), - type = parts.get(2), - scope = parts.get(3), - version = version, - ) + print("Please load `unpack_coordinates` from `@rules_jvm_external//private/lib:coordinates.bzl`.") + return _unpack_coordinates(coords) def _whitespace(indent): whitespace = "" @@ -89,7 +58,7 @@ unversioned_dep_coordinates = [], runtime_deps = [], indent = 8): - unpacked_coordinates = unpack_coordinates(coordinates) + unpacked_coordinates = _unpack_coordinates(coordinates) substitutions = { "{groupId}": unpacked_coordinates.groupId, "{artifactId}": unpacked_coordinates.artifactId, @@ -100,7 +69,7 @@ if parent: # We only want the groupId, artifactID, and version - unpacked_parent = unpack_coordinates(parent) + unpacked_parent = _unpack_coordinates(parent) whitespace = _whitespace(indent - 4) parts = [ @@ -116,7 +85,7 @@ deps = [] for dep in sorted(versioned_dep_coordinates) + sorted(unversioned_dep_coordinates): include_version = dep in versioned_dep_coordinates - unpacked = unpack_coordinates(dep) + unpacked = _unpack_coordinates(dep) new_scope = "runtime" if dep in runtime_deps else unpacked.scope unpacked = struct( groupId = unpacked.groupId,
diff --git a/providers.bzl b/providers.bzl new file mode 100644 index 0000000..21d7944 --- /dev/null +++ b/providers.bzl
@@ -0,0 +1,6 @@ +load("//private/rules:has_maven_deps.bzl", _MavenHintInfo = "MavenHintInfo", _MavenInfo = "MavenInfo") +load("//private/rules:maven_publish.bzl", _MavenPublishInfo = "MavenPublishInfo") + +MavenHintInfo = _MavenHintInfo +MavenInfo = _MavenInfo +MavenPublishInfo = _MavenPublishInfo
diff --git a/repositories.bzl b/repositories.bzl index 77c304f..a923f60 100644 --- a/repositories.bzl +++ b/repositories.bzl
@@ -47,6 +47,16 @@ sha256 = "eb5447f019734b0c4284eaa5f8248415084da5445ba8201c935a211ab8af43a0", ) + maybe( + http_archive, + name = "rules_license", + urls = [ + "https://mirror.bazel.build/github.com/bazelbuild/rules_license/releases/download/1.0.0/rules_license-1.0.0.tar.gz", + "https://github.com/bazelbuild/rules_license/releases/download/1.0.0/rules_license-1.0.0.tar.gz", + ], + sha256 = "26d4021f6898e23b82ef953078389dd49ac2b5618ac564ade4ef87cced147b38", + ) + maven_install( name = "rules_jvm_external_deps", artifacts = [
diff --git a/tests/unit/jvm_import/jvm_import_test.bzl b/tests/unit/jvm_import/jvm_import_test.bzl index bc0199a..4e6316a 100644 --- a/tests/unit/jvm_import/jvm_import_test.bzl +++ b/tests/unit/jvm_import/jvm_import_test.bzl
@@ -17,6 +17,9 @@ """ load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") +load("@rules_license//rules:providers.bzl", "PackageInfo") +load("@rules_license//rules:gather_metadata.bzl", "gather_metadata_info") +load("@rules_license//rules_gathering:gathering_providers.bzl", "TransitiveMetadataInfo") TagsInfo = provider( doc = "Provider to propagate jvm_import's tags for testing purposes", @@ -59,12 +62,71 @@ }, ) +def _does_jvm_import_export_a_package_provider_impl(ctx): + env = analysistest.begin(ctx) + + asserts.true(env, PackageInfo in ctx.attr.src) + package_info = ctx.attr.src[PackageInfo] + asserts.equals(env, "pkg:maven/com.google.code.findbugs:jsr305@3.0.2", package_info.purl) + + # The metadata is applied directly to the target in this case, so there should + # not be any transitive metadata. Apparently. +# TODO: restore once https://github.com/bazelbuild/rules_license/issues/154 is resolved +# metadata_info = ctx.attr.src[TransitiveMetadataInfo] +# asserts.equals(env, depset(), metadata_info.package_info) + + return analysistest.end(env) + +does_jvm_import_export_a_package_provider_test = analysistest.make( + _does_jvm_import_export_a_package_provider_impl, + attrs = { + "src": attr.label( + doc = "Target to traverse for providers", + aspects = [gather_metadata_info], + mandatory = True, + ) + } +) + +def _does_non_jvm_import_target_carry_metadata(ctx): + env = analysistest.begin(ctx) + + asserts.false(env, PackageInfo in ctx.attr.src) + + metadata_info = ctx.attr.src[TransitiveMetadataInfo] + infos = metadata_info.package_info.to_list() + asserts.equals(env, 1, len(infos)) + + return analysistest.end(env) + +does_non_jvm_import_target_carry_metadata_test = analysistest.make( + _does_non_jvm_import_target_carry_metadata, + attrs = { + "src": attr.label( + doc = "Target to traverse for providers", + aspects = [gather_metadata_info], + mandatory = True, + ) + } +) + def jvm_import_test_suite(name): does_jvm_import_have_tags_test( name = "does_jvm_import_have_tags_test", target_under_test = "@jvm_import_test//:com_google_code_findbugs_jsr305_3_0_2", src = "@jvm_import_test//:com_google_code_findbugs_jsr305_3_0_2", ) + does_jvm_import_export_a_package_provider_test( + name = "does_jvm_import_export_a_package_provider", + target_under_test = "@jvm_import_test//:com_google_code_findbugs_jsr305", + src = "@jvm_import_test//:com_google_code_findbugs_jsr305", + ) +# TODO: restore once https://github.com/bazelbuild/rules_license/issues/154 is resolved +# does_non_jvm_import_target_carry_metadata_test( +# name = "does_non_jvm_import_target_carry_metadata", +# target_under_test = "@jvm_import_test//:com_android_support_appcompat_v7", +# src = "@jvm_import_test//:com_android_support_appcompat_v7", +# ) native.test_suite( name = name, tests = [