diff --git a/src/test/java/com/google/devtools/build/lib/rules/java/BUILD b/src/test/java/com/google/devtools/build/lib/rules/java/BUILD index d5bbf9a556d350..e89b2dbc8f22ef 100644 --- a/src/test/java/com/google/devtools/build/lib/rules/java/BUILD +++ b/src/test/java/com/google/devtools/build/lib/rules/java/BUILD @@ -12,6 +12,22 @@ filegroup( visibility = ["//src:__subpackages__"], ) +java_test( + name = "JavaInfoRoundtripTest", + srcs = ["JavaInfoRoundtripTest.java"], + data = ["//tools/build_defs/inspect:defs"], + deps = [ + "//src/main/java/com/google/devtools/build/lib/analysis:configured_target", + "//src/main/java/com/google/devtools/build/lib/vfs", + "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment", + "//src/main/java/net/starlark/java/eval", + "//src/test/java/com/google/devtools/build/lib/analysis/util", + "//src/test/java/com/google/devtools/build/lib/testutil:TestConstants", + "//third_party:junit4", + "//third_party:truth", + ], +) + java_test( name = "JavaInfoStarlarkApiTest", srcs = ["JavaInfoStarlarkApiTest.java"], diff --git a/src/test/java/com/google/devtools/build/lib/rules/java/JavaInfoRoundtripTest.java b/src/test/java/com/google/devtools/build/lib/rules/java/JavaInfoRoundtripTest.java new file mode 100644 index 00000000000000..dcf9573f58fbfb --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/rules/java/JavaInfoRoundtripTest.java @@ -0,0 +1,263 @@ +// Copyright 2021 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.devtools.build.lib.rules.java; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.devtools.build.lib.analysis.ConfiguredTarget; +import com.google.devtools.build.lib.analysis.util.BuildViewTestCase; +import com.google.devtools.build.lib.testutil.TestConstants; +import com.google.devtools.build.lib.vfs.ModifiedFileSet; +import com.google.devtools.build.lib.vfs.PathFragment; +import com.google.devtools.build.lib.vfs.Root; +import java.util.Map; +import net.starlark.java.eval.Dict; +import net.starlark.java.eval.Starlark; +import net.starlark.java.eval.StarlarkList; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests if JavaInfo identical to one returned by Java rules can be constructed. */ +@RunWith(JUnit4.class) +public class JavaInfoRoundtripTest extends BuildViewTestCase { + /** A rule to convert JavaInfo to a structure having only string values. */ + @Before + public void javaInfoToDict() throws Exception { + mockToolsConfig.create("tools/build_defs/inspect/BUILD"); + mockToolsConfig.copyTool( + TestConstants.BAZEL_REPO_SCRATCH + "tools/build_defs/inspect/struct_to_dict.bzl", + "tools/build_defs/inspect/struct_to_dict.bzl"); + + scratch.file( + "javainfo/javainfo_to_dict.bzl", + "load('//tools/build_defs/inspect:struct_to_dict.bzl', 'struct_to_dict')", + "def _impl(ctx):", + " return struct(result = struct_to_dict(ctx.attr.dep[JavaInfo], 10))", + "javainfo_to_dict = rule(_impl, attrs = {'dep' : attr.label()})"); + } + + /** A simple rule that calls JavaInfo constructor using identical attribute as java_library. */ + @Before + public void constructJavaInfo() throws Exception { + setBuildLanguageOptions("--experimental_google_legacy_api"); + + scratch.file( + "foo/construct_javainfo.bzl", + "def _impl(ctx):", + " OUTS = {", + " 'lib':'lib%s.jar',", + " 'hjar': 'lib%s-hjar.jar',", + " 'src': 'lib%s-src.jar',", + " 'compile_jdeps': 'lib%s-hjar.jdeps',", + " 'jdeps': 'lib%s.jdeps',", + " 'manifest': 'lib%s.jar_manifest_proto'}", + " for file, name in OUTS.items():", + " OUTS[file] = ctx.actions.declare_file(name % ctx.label.name)", + " ctx.actions.write(OUTS[file], '')", + " ", + " java_info = JavaInfo(", + " output_jar = OUTS['lib'],", + " compile_jar = OUTS['hjar'],", + " source_jar = OUTS['src'],", + " deps = [d[JavaInfo] for d in ctx.attr.deps],", + " runtime_deps = [d[JavaInfo] for d in ctx.attr.runtime_deps],", + " exports = [d[JavaInfo] for d in ctx.attr.exports],", + " jdeps = OUTS['jdeps'],", + " )", + " return [java_info]", + "", + "construct_javainfo = rule(", + " implementation = _impl,", + " attrs = {", + " 'srcs': attr.label_list(allow_files = True),", + " 'deps': attr.label_list(),", + " 'runtime_deps': attr.label_list(),", + " 'exports': attr.label_list(),", + " },", + " fragments = ['java'],", + ")"); + } + + /** For a given target providing JavaInfo returns a Starlark Dict with String values */ + private Dict getDictFromJavaInfo(String packageName, String javaInfoTarget) + throws Exception { + // Because we're overwriting files to have identical names, we need to invalidate them. + skyframeExecutor.invalidateFilesUnderPathForTesting( + reporter, + new ModifiedFileSet.Builder().modify(PathFragment.create(packageName + "/BUILD")).build(), + Root.fromPath(rootDirectory)); + + scratch.deleteFile("javainfo/BUILD"); + ConfiguredTarget dictTarget = + scratchConfiguredTarget( + "javainfo", + "javainfo", + "load(':javainfo_to_dict.bzl', 'javainfo_to_dict')", + "javainfo_to_dict(", + " name = 'javainfo',", + " dep = '//" + packageName + ':' + javaInfoTarget + "',", + ")"); + @SuppressWarnings("unchecked") // deserialization + Dict javaInfo = (Dict) dictTarget.get("result"); + return javaInfo; + } + + // TODO(b/163811682): remove once JavaInfo supports setting manifest_proto and native_headers. + private Dict removeManifestAndNativeHeaders(Dict javaInfo) { + @SuppressWarnings("unchecked") // safe by specification + Dict outputs = (Dict) javaInfo.get("outputs"); + @SuppressWarnings("unchecked") // safe by specification + StarlarkList jars = (StarlarkList) outputs.get("jars"); + @SuppressWarnings("unchecked") // safe by specification + Dict jar0 = (Dict) jars.get(0); + + jar0 = Dict.builder().putAll(jar0).put("manifest_proto", Starlark.NONE).buildImmutable(); + jars = StarlarkList.immutableOf(jar0); + outputs = + Dict.builder() + .putAll((Map) javaInfo.get("outputs")) + .put("jars", jars) + .put("native_headers", Starlark.NONE) + .buildImmutable(); + return Dict.builder().putAll(javaInfo).put("outputs", outputs).buildImmutable(); + } + + private Dict removeCompilationInfo(Dict javaInfo) { + return Dict.builder().putAll(javaInfo).put("compilation_info", Starlark.NONE).buildImmutable(); + } + + @Test + public void dictFromJavaInfo_nonEmpty() throws Exception { + scratch.overwriteFile("foo/BUILD", "java_library(name = 'java_lib', srcs = ['A.java'])"); + + Dict javaInfo = getDictFromJavaInfo("foo", "java_lib"); + + assertThat((Map) javaInfo).isNotEmpty(); + } + + @Test + public void dictFromJavaInfo_detectsDifference() throws Exception { + + scratch.overwriteFile("foo/BUILD", "java_library(name = 'java_lib', srcs = ['A.java'])"); + Dict javaInfoA = getDictFromJavaInfo("foo", "java_lib"); + + scratch.overwriteFile("foo/BUILD", "java_library(name = 'java_lib2', srcs = ['A.java'])"); + Dict javaInfoB = getDictFromJavaInfo("foo", "java_lib2"); + + assertThat((Map) javaInfoA).isNotEqualTo(javaInfoB); + } + + @Test + public void roundtripJavainfo_srcs() throws Exception { + + scratch.overwriteFile("foo/BUILD", "java_library(name = 'java_lib', srcs = ['A.java'])"); + Dict javaInfoA = getDictFromJavaInfo("foo", "java_lib"); + scratch.overwriteFile( + "foo/BUILD", + "load('//foo:construct_javainfo.bzl', 'construct_javainfo')", + "construct_javainfo(name = 'java_lib', srcs = ['A.java'])"); + Dict javaInfoB = getDictFromJavaInfo("foo", "java_lib"); + + javaInfoA = removeManifestAndNativeHeaders(javaInfoA); + javaInfoA = removeCompilationInfo(javaInfoA); + assertThat((Map) javaInfoB).isEqualTo(javaInfoA); + } + + @Test + public void roundtripJavaInfo_deps() throws Exception { + scratch.file("bar/BUILD", "java_library(name = 'javalib', srcs = ['A.java'])"); + + scratch.overwriteFile( + "foo/BUILD", + "java_library(", + " name = 'java_lib',", + " srcs = ['A.java'],", + " deps = ['//bar:javalib']", + ")"); + Dict javaInfoA = getDictFromJavaInfo("foo", "java_lib"); + scratch.overwriteFile( + "foo/BUILD", + "load('//foo:construct_javainfo.bzl', 'construct_javainfo')", + "construct_javainfo(", + " name = 'java_lib', ", + " srcs = ['A.java'], ", + " deps = ['//bar:javalib'],", + ")"); + Dict javaInfoB = getDictFromJavaInfo("foo", "java_lib"); + + javaInfoA = removeManifestAndNativeHeaders(javaInfoA); + javaInfoA = removeCompilationInfo(javaInfoA); + assertThat((Map) javaInfoB).isEqualTo(javaInfoA); + } + + @Test + public void roundtipJavaInfo_runtimeDeps() throws Exception { + scratch.file("bar/BUILD", "java_library(name = 'deplib', srcs = ['A.java'])"); + + scratch.overwriteFile( + "foo/BUILD", + "java_library(", + " name = 'java_lib',", + " srcs = ['A.java'],", + " runtime_deps = ['//bar:deplib']", + ")"); + Dict javaInfoA = getDictFromJavaInfo("foo", "java_lib"); + scratch.overwriteFile( + "foo/BUILD", + "load('//foo:construct_javainfo.bzl', 'construct_javainfo')", + "construct_javainfo(", + " name = 'java_lib', ", + " srcs = ['A.java'], ", + " runtime_deps = ['//bar:deplib'],", + ")"); + Dict javaInfoB = getDictFromJavaInfo("foo", "java_lib"); + + javaInfoA = removeManifestAndNativeHeaders(javaInfoA); + javaInfoA = removeCompilationInfo(javaInfoA); + assertThat((Map) javaInfoB).isEqualTo(javaInfoA); + } + + // TODO(b/159269546): enable once JavaInfo call is fixed to return JavaExportsProvider") + @Test + @Ignore + public void roundtipJavaInfo_exports() throws Exception { + scratch.file("bar/BUILD", "java_library(name = 'exportlib', srcs = ['A.java'])"); + + scratch.overwriteFile( + "foo/BUILD", + "java_library(", + " name = 'java_lib',", + " srcs = ['A.java'],", + " exports = ['//bar:exportlib']", + ")"); + Dict javaInfoA = getDictFromJavaInfo("foo", "java_lib"); + scratch.overwriteFile( + "foo/BUILD", + "load('//foo:construct_javainfo.bzl', 'construct_javainfo')", + "construct_javainfo(", + " name = 'java_lib', ", + " srcs = ['A.java'], ", + " exports = ['//bar:exportlib'],", + ")"); + Dict javaInfoB = getDictFromJavaInfo("foo", "java_lib"); + + javaInfoA = removeManifestAndNativeHeaders(javaInfoA); + javaInfoA = removeCompilationInfo(javaInfoA); + assertThat((Map) javaInfoB).isEqualTo(javaInfoA); + } +} diff --git a/tools/build_defs/BUILD b/tools/build_defs/BUILD index 947c5e51b2c5db..9cef8aeda9e7af 100644 --- a/tools/build_defs/BUILD +++ b/tools/build_defs/BUILD @@ -5,6 +5,7 @@ filegroup( "//tools/build_defs/cc/whitelists/parse_headers_and_layering_check:srcs", "//tools/build_defs/cc/whitelists/starlark_hdrs_check:srcs", "//tools/build_defs/hash:srcs", + "//tools/build_defs/inspect:srcs", "//tools/build_defs/pkg:srcs", "//tools/build_defs/repo:srcs", "//tools/build_defs/cc/tests:cc_import_tests_files", diff --git a/tools/build_defs/inspect/BUILD b/tools/build_defs/inspect/BUILD new file mode 100644 index 00000000000000..ccf685dff91c5e --- /dev/null +++ b/tools/build_defs/inspect/BUILD @@ -0,0 +1,15 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +licenses(["notice"]) + +filegroup( + name = "srcs", + srcs = glob(["**"]), + visibility = ["//tools/build_defs:__pkg__"], +) + +bzl_library( + name = "defs", + srcs = glob(["*.bzl"]), + visibility = ["//visibility:public"], +) diff --git a/tools/build_defs/inspect/struct_to_dict.bzl b/tools/build_defs/inspect/struct_to_dict.bzl new file mode 100644 index 00000000000000..d46fe27b55c813 --- /dev/null +++ b/tools/build_defs/inspect/struct_to_dict.bzl @@ -0,0 +1,85 @@ +# Copyright 2021 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""struct_to_dict() tries to convert struct-like objects to dicts "recursively". + +Useful to dump arbitrary provider data. Objects below the +specified depth are copied literally. +""" + +def struct_to_dict(x, depth = 5): + root = {} + queue = [(root, x)] + for i in range(depth): + nextlevel = [] if i < depth - 1 else None + for dest, obj in queue: + if _is_depset(obj): + obj = obj.to_list() + if _is_list(obj): + for item in list(obj): + converted = _convert_one(item, nextlevel) + dest.append(converted) + elif type(obj) == type({}): + for key, value in dest.items(): + converted = _convert_one(value, nextlevel) + dest[key] = converted + else: # struct or object + dest["_type"] = type(obj) + for propname in dir(obj): + _token = struct() + value = getattr(obj, propname, _token) + if value == _token: + continue # Native methods are not inspectable. Ignore. + converted = _convert_one(value, nextlevel) + dest[propname] = converted + if type(obj) == "Target": + if JavaInfo in obj: + dest["JavaInfo"] = _convert_one(obj[JavaInfo], nextlevel) + if CcInfo in obj: + dest["CcInfo"] = _convert_one(obj[CcInfo], nextlevel) + + queue = nextlevel + return root + +def _convert_one(val, nextlevel): + nest = nextlevel != None + if _is_sequence(val) and nest: + out = [] + nextlevel.append((out, val)) + return out + elif _is_atom(val) or not nest: + return val + elif type(val) == "File": + return val.path + elif type(val) == "Label": + return str(val) + else: # by default try to convert object to dict + out = {} + nextlevel.append((out, val)) + return out + +def _is_sequence(val): + return _is_list(val) or _is_depset(val) + +def _is_list(val): + return type(val) == type([]) + +def _is_depset(val): + return type(val) == type(depset()) + +def _is_atom(val): + return (type(val) == type("") or + type(val) == type(0) or + type(val) == type(False) or + type(val) == type(None))