Skip to content

Commit d7f34c1

Browse files
authored
Merge pull request #214 from salesforce/plaird/deps_filter
Add the deps filter mechanism and update docs and examples.
2 parents e196a8e + e26e42d commit d7f34c1

File tree

4 files changed

+202
-15
lines changed

4 files changed

+202
-15
lines changed

examples/demoapp/BUILD

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# Copyright (c) 2017-2021, salesforce.com, inc.
2+
# Copyright (c) 2017-2024, salesforce.com, inc.
33
# All rights reserved.
44
# Licensed under the BSD 3-Clause license.
55
# For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
@@ -13,8 +13,12 @@
1313
# load our Spring Boot rule
1414
load("//springboot:springboot.bzl", "springboot")
1515

16+
# load the deps filter
17+
load("//springboot:deps_filter_transitive.bzl", "deps_filter_transitive")
18+
19+
1620
# dependencies from other packages in the workspace
17-
lib_deps = [
21+
deps = [
1822
"//examples/demoapp/libs/lib1",
1923
"//examples/demoapp/libs/lib2",
2024
]
@@ -27,21 +31,32 @@ springboot_deps = [
2731
"@maven//:org_springframework_boot_spring_boot_loader_tools",
2832
"@maven//:org_springframework_spring_webmvc",
2933

30-
"@maven//:javax_annotation_javax_annotation_api",
31-
3234
# bring in same dep again as above, but through a different maven_install
3335
# rule: the springboot rule does not package duplicate deps, first one wins
3436
"@spring_boot_starter_jetty//:org_springframework_boot_spring_boot_starter_jetty",
3537
]
3638

39+
# Sometimes you have a transitive that you don't want. The unwanted_classes.md doc
40+
# covers this case, and this snippet shows how to use it:
41+
deps_filter_transitive(
42+
name = "filtered_deps",
43+
deps = springboot_deps + deps, # the input list
44+
deps_exclude = [
45+
"@maven//:javax_annotation_javax_annotation_api", # exclude this transitive
46+
],
47+
exclude_transitives = True,
48+
)
49+
3750
# This Java library contains the app code
3851
java_library(
3952
name = "demoapp_lib",
4053
srcs = glob(["src/main/java/**/*.java"]),
4154
resources = glob(["src/main/resources/**"]),
42-
deps = springboot_deps + lib_deps,
55+
deps = [":filtered_deps"],
4356
)
4457

58+
# This is just an example of having a dependency that you want only added to
59+
# the springboot jar, not the java_library. This is rare.
4560
java_library(
4661
name = "rootclassloader_lib",
4762
srcs = glob(["src_root/main/java/**/*.java"]),
@@ -65,7 +80,8 @@ springboot(
6580
java_library = ":demoapp_lib",
6681

6782
# DEPS ARE OPTIONAL HERE
68-
# The springboot rule inherits all deps and runtime_deps from the java_library
83+
# The springboot rule inherits all deps and runtime_deps from the java_library()
84+
# but this jar for uncommon reason is just added to the springboot rule.
6985
deps = [ ":rootclassloader_lib", ],
7086

7187
# TO TEST THE DUPE CLASSES FEATURE:
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
#
2-
# Copyright (c) 2019-2021, salesforce.com, inc.
2+
# Copyright (c) 2019-2024, salesforce.com, inc.
33
# All rights reserved.
44
# Licensed under the BSD 3-Clause license.
55
# For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
#
77

8-
jakarta.annotation-api-1.3.5.jar
9-
javax.annotation-api-1.3.2.jar
10-
spring-jcl-5.2.1.RELEASE.jar
11-
commons-logging-1.2.jar
128
liblib1.jar
139
liblib2.jar

springboot/deps_filter_transitive.bzl

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
def _depaggregator_rule_impl(merged, ctx):
2+
"""
3+
This method processes declared deps and their transitive closures
4+
to assemble a cohesive set of jars essential for the build process. During
5+
this process, it excludes deps specified in 'deps_exclude', which
6+
lists jar labels to be omitted from packaging due to issues that cannot
7+
be resolved upstream. By default, with 'exclude_transitives' set to true, any
8+
transitive deps that are only required by excluded deps
9+
are also omitted, ensuring that only necessary transitives are included
10+
in the final package. It uses 'deps_exclude_paths' to exclude deps
11+
based on partial filename matches, ensuring problematic files are also
12+
excluded from the build. This method ensures that only necessary
13+
deps are included for the build process.
14+
"""
15+
exclude_transitives = ctx.attr.exclude_transitives
16+
17+
# list to store jars to be included and a dictionary to track excluded jars
18+
jars = []
19+
excludes = {}
20+
21+
if exclude_transitives:
22+
# Dictionary to track transitive dependency paths that should be excluded
23+
transitives_excludes = {}
24+
25+
# List to store deps info for deps present in 'deps_exclude'
26+
direct_excludes = []
27+
28+
# Iterate through the deps specified in 'deps_exclude' to collect
29+
# jars that should be excluded from the final set.
30+
31+
for exclusion_info in ctx.attr.deps_exclude:
32+
# For each excluded dependency, add its compile-time JARs to the exclusion list
33+
for compile_jar in exclusion_info[JavaInfo].full_compile_jars.to_list():
34+
excludes[compile_jar.path] = True
35+
36+
if exclude_transitives:
37+
# Mark all transitives of the current dependency as excluded
38+
# This list will be updated later based on transitives of non-excluded deps
39+
direct_excludes.append(str(exclusion_info))
40+
for transitive_jar in exclusion_info[JavaInfo].transitive_runtime_jars.to_list():
41+
transitives_excludes[transitive_jar.path] = True
42+
43+
if exclude_transitives:
44+
# Iterate over all deps, for non-excluded deps, mark their transitives as included.
45+
for deps_info in ctx.attr.deps:
46+
# skip the current dependency if it is listed in 'deps_exclude'.
47+
if str(deps_info) in direct_excludes:
48+
continue
49+
50+
# For non-excluded deps, mark them and their transitive deps as included (not to be excluded)
51+
# (transitive_runtime_jars includes both the primary JAR and its transitive deps)
52+
for transitive_jar in deps_info[JavaInfo].transitive_runtime_jars.to_list():
53+
if transitive_jar.path in transitives_excludes:
54+
transitives_excludes[transitive_jar.path] = False
55+
56+
# update the excludes list
57+
for dep_path in transitives_excludes:
58+
# print("Transitive:", str(dep_path), "is excluded", transitives_excludes[dep_path])
59+
if transitives_excludes[dep_path]:
60+
excludes[dep_path] = True
61+
62+
# compute the final set of jars
63+
for dep in merged.transitive_runtime_jars.to_list():
64+
# If the current JAR is in the exclusion list, skip it (do not include it)
65+
if excludes.get(dep.path, None) != None:
66+
pass
67+
else:
68+
# Default to including the JAR unless a pattern match excludes it
69+
include = True
70+
for pattern in ctx.attr.deps_exclude_paths:
71+
if dep.path.find(pattern) > -1:
72+
include = False
73+
break
74+
if include:
75+
jars.append(dep)
76+
77+
return jars
78+
79+
def _deps_filter_transitive_impl(ctx):
80+
"""
81+
This rule filters out specified deps and JARs from the compile-time
82+
and runtime deps. It utilizes the 'deps_exclude' attribute to omit
83+
specific JAR labels and the 'deps_exclude_paths' attribute to exclude
84+
deps based on partial paths in their filenames. By default, with
85+
'exclude_transitives' set to true, any transitive deps solely required
86+
by the deps in 'deps_exclude' are also excluded. These exclusions ensure
87+
the final collection includes only the necessary elements for the build
88+
process, eliminating problematic deps.
89+
"""
90+
91+
if len(ctx.attr.deps) == 0:
92+
fail("Error: 'deps' cannot be an empty list")
93+
94+
# magical incantation for getting upstream transitive closure of java deps
95+
merged = java_common.merge([dep[java_common.provider] for dep in ctx.attr.deps])
96+
runtime_dep_merged = java_common.merge([runtime_dep[java_common.provider] for runtime_dep in ctx.attr.runtime_deps])
97+
98+
compile_time_jars = _depaggregator_rule_impl(merged, ctx)
99+
runtime_jars = _depaggregator_rule_impl(runtime_dep_merged, ctx)
100+
101+
if len(compile_time_jars) == 0:
102+
fail("Error: The rule must return at least one compile-time JAR. Excluding all compile-time dependencies is not allowed.")
103+
104+
return [
105+
DefaultInfo(files = depset(compile_time_jars,)),
106+
JavaInfo(
107+
compile_jar = None,
108+
output_jar = compile_time_jars[0], # output jar must be non-empty, adding a dummy value to it
109+
exports = [JavaInfo(source_jar = jar, compile_jar = jar, output_jar = jar) for jar in compile_time_jars],
110+
runtime_deps = [JavaInfo(source_jar = jar, compile_jar = jar, output_jar = jar) for jar in
111+
runtime_jars],
112+
deps = [JavaInfo(source_jar = jar, compile_jar = jar, output_jar = jar) for jar in compile_time_jars],
113+
),
114+
]
115+
116+
117+
deps_filter_transitive = rule(
118+
implementation = _deps_filter_transitive_impl,
119+
attrs = {
120+
"deps": attr.label_list(providers = [java_common.provider]),
121+
"runtime_deps": attr.label_list(providers = [java_common.provider], allow_empty = True),
122+
"deps_exclude": attr.label_list(providers = [java_common.provider], allow_empty = True),
123+
"deps_exclude_paths": attr.string_list(),
124+
"exclude_transitives": attr.bool(default = True),
125+
},
126+
)

springboot/unwanted_classes.md

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,59 @@ Bazel query is the best way to do this:
8787
bazel query 'somepath(//examples/helloworld:helloworld_lib, "@maven//:org_springframework_spring_webmvc")'
8888
```
8989

90-
### Removing Unwanted Classes with an Exclude List
90+
### Removing Unwanted Classes with a Filter
91+
92+
The next best way to exclude dependencies is to remove them before they are added to
93+
the ```java_library``` rule invocation.
94+
This mechanism is not specific to ```springboot``` at all - it is a sample provided
95+
by rules_spring because it is a commmon use case.
96+
The benefits to excluding dependencies this way (as opposed to the exclude lists)
97+
is that your test execution will use the actual classpath set into
98+
the ```springboot``` executable jar.
99+
100+
In this example, the springboot jar wants the red, green and blue libraries, but
101+
does not want the yellow library (a transitive).
102+
```starlark
103+
load("//springboot:deps_filter_transitive.bzl", "deps_filter_transitive")
104+
105+
deps = [
106+
"@maven//:com_colors_red",
107+
"@maven//:com_colors_green",
108+
"@maven//:com_colors_blue",
109+
]
110+
111+
deps_filter_transitive(
112+
name = "filtered_deps",
113+
deps = deps, # input list of deps
114+
deps_exclude = [
115+
"@maven//:com_colors_yellow", # yellow is a transitive of green, and we don't want it
116+
],
117+
exclude_transitives = True, # also exclude any transitive of yellow
118+
)
119+
120+
java_library(
121+
name = "base_lib",
122+
deps = [":filtered_deps"], # the filtered deps has yellow removed
123+
...
124+
)
125+
126+
springboot(
127+
...
128+
java_library = ":base_lib",
129+
...
130+
)
131+
```
132+
133+
134+
### Removing Unwanted Classes with an Exclude List (deprecated)
135+
136+
An exclude list can be passed to the Spring Boot rule which will prevent that dependency
137+
from being copied into the jar during the packaging step.
138+
This was our original mechanism of removing dependencies from the dependency graph.
91139

92-
In some cases you do not have the control to remove the dependency from the dependency graph.
93-
An exclude list can be passed to the Spring Boot rule which will prevent that dependency from being copied into the jar.
94-
This is the second best approach for handling unwanted classes.
140+
:warning: The Exclude list approach is not recommended. The Filter list is more accurate.
141+
With Exclude lists, your tests will run without the exclusions, such that your test classpath
142+
will not match what will run with your executable jar.
95143

96144
There are two forms: *deps_exclude* and *deps_exclude_paths*.
97145
- *deps_exclude* uses Bazel labels to match the desired target to exclude.
@@ -106,6 +154,7 @@ The path approach is easier for these cases.
106154
It is used like this:
107155

108156
```starlark
157+
# WARNING: This is an obsolete example. Use the filter mechanism instead.
109158
springboot(
110159
name = "helloworld",
111160
boot_app_class = "com.sample.SampleMain",

0 commit comments

Comments
 (0)