Skip to content

Commit

Permalink
Merge pull request #214 from salesforce/plaird/deps_filter
Browse files Browse the repository at this point in the history
Add the deps filter mechanism and update docs and examples.
  • Loading branch information
plaird authored Dec 3, 2024
2 parents e196a8e + e26e42d commit d7f34c1
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 15 deletions.
28 changes: 22 additions & 6 deletions examples/demoapp/BUILD
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# Copyright (c) 2017-2021, salesforce.com, inc.
# Copyright (c) 2017-2024, salesforce.com, inc.
# All rights reserved.
# Licensed under the BSD 3-Clause license.
# For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
Expand All @@ -13,8 +13,12 @@
# load our Spring Boot rule
load("//springboot:springboot.bzl", "springboot")

# load the deps filter
load("//springboot:deps_filter_transitive.bzl", "deps_filter_transitive")


# dependencies from other packages in the workspace
lib_deps = [
deps = [
"//examples/demoapp/libs/lib1",
"//examples/demoapp/libs/lib2",
]
Expand All @@ -27,21 +31,32 @@ springboot_deps = [
"@maven//:org_springframework_boot_spring_boot_loader_tools",
"@maven//:org_springframework_spring_webmvc",

"@maven//:javax_annotation_javax_annotation_api",

# bring in same dep again as above, but through a different maven_install
# rule: the springboot rule does not package duplicate deps, first one wins
"@spring_boot_starter_jetty//:org_springframework_boot_spring_boot_starter_jetty",
]

# Sometimes you have a transitive that you don't want. The unwanted_classes.md doc
# covers this case, and this snippet shows how to use it:
deps_filter_transitive(
name = "filtered_deps",
deps = springboot_deps + deps, # the input list
deps_exclude = [
"@maven//:javax_annotation_javax_annotation_api", # exclude this transitive
],
exclude_transitives = True,
)

# This Java library contains the app code
java_library(
name = "demoapp_lib",
srcs = glob(["src/main/java/**/*.java"]),
resources = glob(["src/main/resources/**"]),
deps = springboot_deps + lib_deps,
deps = [":filtered_deps"],
)

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

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

# TO TEST THE DUPE CLASSES FEATURE:
Expand Down
6 changes: 1 addition & 5 deletions examples/demoapp/demoapp_dupeclass_allowlist.txt
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
#
# Copyright (c) 2019-2021, salesforce.com, inc.
# Copyright (c) 2019-2024, salesforce.com, inc.
# All rights reserved.
# Licensed under the BSD 3-Clause license.
# For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
#

jakarta.annotation-api-1.3.5.jar
javax.annotation-api-1.3.2.jar
spring-jcl-5.2.1.RELEASE.jar
commons-logging-1.2.jar
liblib1.jar
liblib2.jar
126 changes: 126 additions & 0 deletions springboot/deps_filter_transitive.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
def _depaggregator_rule_impl(merged, ctx):
"""
This method processes declared deps and their transitive closures
to assemble a cohesive set of jars essential for the build process. During
this process, it excludes deps specified in 'deps_exclude', which
lists jar labels to be omitted from packaging due to issues that cannot
be resolved upstream. By default, with 'exclude_transitives' set to true, any
transitive deps that are only required by excluded deps
are also omitted, ensuring that only necessary transitives are included
in the final package. It uses 'deps_exclude_paths' to exclude deps
based on partial filename matches, ensuring problematic files are also
excluded from the build. This method ensures that only necessary
deps are included for the build process.
"""
exclude_transitives = ctx.attr.exclude_transitives

# list to store jars to be included and a dictionary to track excluded jars
jars = []
excludes = {}

if exclude_transitives:
# Dictionary to track transitive dependency paths that should be excluded
transitives_excludes = {}

# List to store deps info for deps present in 'deps_exclude'
direct_excludes = []

# Iterate through the deps specified in 'deps_exclude' to collect
# jars that should be excluded from the final set.

for exclusion_info in ctx.attr.deps_exclude:
# For each excluded dependency, add its compile-time JARs to the exclusion list
for compile_jar in exclusion_info[JavaInfo].full_compile_jars.to_list():
excludes[compile_jar.path] = True

if exclude_transitives:
# Mark all transitives of the current dependency as excluded
# This list will be updated later based on transitives of non-excluded deps
direct_excludes.append(str(exclusion_info))
for transitive_jar in exclusion_info[JavaInfo].transitive_runtime_jars.to_list():
transitives_excludes[transitive_jar.path] = True

if exclude_transitives:
# Iterate over all deps, for non-excluded deps, mark their transitives as included.
for deps_info in ctx.attr.deps:
# skip the current dependency if it is listed in 'deps_exclude'.
if str(deps_info) in direct_excludes:
continue

# For non-excluded deps, mark them and their transitive deps as included (not to be excluded)
# (transitive_runtime_jars includes both the primary JAR and its transitive deps)
for transitive_jar in deps_info[JavaInfo].transitive_runtime_jars.to_list():
if transitive_jar.path in transitives_excludes:
transitives_excludes[transitive_jar.path] = False

# update the excludes list
for dep_path in transitives_excludes:
# print("Transitive:", str(dep_path), "is excluded", transitives_excludes[dep_path])
if transitives_excludes[dep_path]:
excludes[dep_path] = True

# compute the final set of jars
for dep in merged.transitive_runtime_jars.to_list():
# If the current JAR is in the exclusion list, skip it (do not include it)
if excludes.get(dep.path, None) != None:
pass
else:
# Default to including the JAR unless a pattern match excludes it
include = True
for pattern in ctx.attr.deps_exclude_paths:
if dep.path.find(pattern) > -1:
include = False
break
if include:
jars.append(dep)

return jars

def _deps_filter_transitive_impl(ctx):
"""
This rule filters out specified deps and JARs from the compile-time
and runtime deps. It utilizes the 'deps_exclude' attribute to omit
specific JAR labels and the 'deps_exclude_paths' attribute to exclude
deps based on partial paths in their filenames. By default, with
'exclude_transitives' set to true, any transitive deps solely required
by the deps in 'deps_exclude' are also excluded. These exclusions ensure
the final collection includes only the necessary elements for the build
process, eliminating problematic deps.
"""

if len(ctx.attr.deps) == 0:
fail("Error: 'deps' cannot be an empty list")

# magical incantation for getting upstream transitive closure of java deps
merged = java_common.merge([dep[java_common.provider] for dep in ctx.attr.deps])
runtime_dep_merged = java_common.merge([runtime_dep[java_common.provider] for runtime_dep in ctx.attr.runtime_deps])

compile_time_jars = _depaggregator_rule_impl(merged, ctx)
runtime_jars = _depaggregator_rule_impl(runtime_dep_merged, ctx)

if len(compile_time_jars) == 0:
fail("Error: The rule must return at least one compile-time JAR. Excluding all compile-time dependencies is not allowed.")

return [
DefaultInfo(files = depset(compile_time_jars,)),
JavaInfo(
compile_jar = None,
output_jar = compile_time_jars[0], # output jar must be non-empty, adding a dummy value to it
exports = [JavaInfo(source_jar = jar, compile_jar = jar, output_jar = jar) for jar in compile_time_jars],
runtime_deps = [JavaInfo(source_jar = jar, compile_jar = jar, output_jar = jar) for jar in
runtime_jars],
deps = [JavaInfo(source_jar = jar, compile_jar = jar, output_jar = jar) for jar in compile_time_jars],
),
]


deps_filter_transitive = rule(
implementation = _deps_filter_transitive_impl,
attrs = {
"deps": attr.label_list(providers = [java_common.provider]),
"runtime_deps": attr.label_list(providers = [java_common.provider], allow_empty = True),
"deps_exclude": attr.label_list(providers = [java_common.provider], allow_empty = True),
"deps_exclude_paths": attr.string_list(),
"exclude_transitives": attr.bool(default = True),
},
)
57 changes: 53 additions & 4 deletions springboot/unwanted_classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,59 @@ Bazel query is the best way to do this:
bazel query 'somepath(//examples/helloworld:helloworld_lib, "@maven//:org_springframework_spring_webmvc")'
```

### Removing Unwanted Classes with an Exclude List
### Removing Unwanted Classes with a Filter

The next best way to exclude dependencies is to remove them before they are added to
the ```java_library``` rule invocation.
This mechanism is not specific to ```springboot``` at all - it is a sample provided
by rules_spring because it is a commmon use case.
The benefits to excluding dependencies this way (as opposed to the exclude lists)
is that your test execution will use the actual classpath set into
the ```springboot``` executable jar.

In this example, the springboot jar wants the red, green and blue libraries, but
does not want the yellow library (a transitive).
```starlark
load("//springboot:deps_filter_transitive.bzl", "deps_filter_transitive")

deps = [
"@maven//:com_colors_red",
"@maven//:com_colors_green",
"@maven//:com_colors_blue",
]

deps_filter_transitive(
name = "filtered_deps",
deps = deps, # input list of deps
deps_exclude = [
"@maven//:com_colors_yellow", # yellow is a transitive of green, and we don't want it
],
exclude_transitives = True, # also exclude any transitive of yellow
)

java_library(
name = "base_lib",
deps = [":filtered_deps"], # the filtered deps has yellow removed
...
)

springboot(
...
java_library = ":base_lib",
...
)
```


### Removing Unwanted Classes with an Exclude List (deprecated)

An exclude list can be passed to the Spring Boot rule which will prevent that dependency
from being copied into the jar during the packaging step.
This was our original mechanism of removing dependencies from the dependency graph.

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

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

```starlark
# WARNING: This is an obsolete example. Use the filter mechanism instead.
springboot(
name = "helloworld",
boot_app_class = "com.sample.SampleMain",
Expand Down

0 comments on commit d7f34c1

Please sign in to comment.