mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 23:35:43 +00:00
Split Eagle server fat JAR into deps + app Docker layers (#6717)
The Docker Build and Push workflow spends more time pushing than building because the entire ~115MB Eagle classpath ships as one pkg_tar layer, so every Scala commit re-uploads the whole blob to DOCR. Add a jar_split rule that partitions the scala_binary's transitive runtime jars by workspace into a stable third-party layer (76MB, 137 jars) and a first-party layer (38MB, 989 jars). crane skips the unchanged deps blob, so typical Scala-only commits re-upload ~38MB instead of ~115MB. Entrypoint switches from -jar fat.jar to a -cp classpath glob over both layer dirs. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+38
-7
@@ -1,5 +1,6 @@
|
||||
load("@rules_oci//oci:defs.bzl", "oci_image", "oci_load", "oci_push")
|
||||
load("@rules_pkg//pkg:tar.bzl", "pkg_tar")
|
||||
load("//ci:jar_split.bzl", "jar_split")
|
||||
|
||||
#
|
||||
# Deployment artifacts (tools needed on the host, not in containers)
|
||||
@@ -50,11 +51,37 @@ pkg_tar(
|
||||
# Push: bazel run //ci:eagle_server_push
|
||||
#
|
||||
|
||||
# Package the deploy JAR
|
||||
# Split the Eagle server runtime classpath into a stable third-party layer
|
||||
# (Scala stdlib, gRPC, Netty, ScalaPB, AWS SDK, ...) and a small first-party
|
||||
# layer that changes every commit. Most pushes then only re-upload the small
|
||||
# app layer instead of the whole ~150-300MB fat JAR.
|
||||
jar_split(
|
||||
name = "eagle_server_jars",
|
||||
binary = "//src/main/scala/net/eagle0/eagle:eagle_server",
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "eagle_server_deps_jars",
|
||||
srcs = [":eagle_server_jars"],
|
||||
output_group = "deps",
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "eagle_server_app_jars",
|
||||
srcs = [":eagle_server_jars"],
|
||||
output_group = "app",
|
||||
)
|
||||
|
||||
pkg_tar(
|
||||
name = "eagle_server_jar_layer",
|
||||
srcs = ["//src/main/scala/net/eagle0/eagle:eagle_server_deploy.jar"],
|
||||
package_dir = "/app",
|
||||
name = "eagle_server_deps_layer",
|
||||
srcs = [":eagle_server_deps_jars"],
|
||||
package_dir = "/app/lib/deps",
|
||||
)
|
||||
|
||||
pkg_tar(
|
||||
name = "eagle_server_app_layer",
|
||||
srcs = [":eagle_server_app_jars"],
|
||||
package_dir = "/app/lib/app",
|
||||
)
|
||||
|
||||
# Package the game resources needed at runtime
|
||||
@@ -82,8 +109,11 @@ oci_image(
|
||||
"-XX:+UnlockDiagnosticVMOptions",
|
||||
"-XX:+DebugNonSafepoints", # Required for JFR to see through inlined methods
|
||||
"-XX:FlightRecorderOptions=stackdepth=256",
|
||||
"-jar",
|
||||
"/app/eagle_server_deploy.jar",
|
||||
# Classpath glob is expanded by the JVM itself (exec-form, no shell).
|
||||
# Deps dir first keeps third-party precedence; app last shadows nothing.
|
||||
"-cp",
|
||||
"/app/lib/deps/*:/app/lib/app/*",
|
||||
"net.eagle0.eagle.Main",
|
||||
],
|
||||
env = {
|
||||
"JAVA_OPTS": "-Xmx2g -XX:+UseG1GC",
|
||||
@@ -91,7 +121,8 @@ oci_image(
|
||||
exposed_ports = ["40032/tcp"],
|
||||
tars = [
|
||||
":busybox_layer",
|
||||
":eagle_server_jar_layer",
|
||||
":eagle_server_deps_layer",
|
||||
":eagle_server_app_layer",
|
||||
":eagle_resources_layer",
|
||||
],
|
||||
workdir = "/app",
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Split a JVM binary's runtime classpath into first-party and third-party jar sets.
|
||||
|
||||
This exists so the Docker image can place rarely-changing third-party jars in a
|
||||
lower OCI layer and frequently-changing first-party jars in a small top layer,
|
||||
so most pushes only re-upload the small layer.
|
||||
"""
|
||||
|
||||
def _unique_name(jar):
|
||||
# short_path is unique per jar and stable across commits (it depends only on
|
||||
# the jar's own package/coordinate, not on unrelated targets), so the deps
|
||||
# layer's tar entries stay byte-identical and its blob digest stays cached.
|
||||
path = jar.short_path
|
||||
if path.startswith("../"):
|
||||
path = path[3:]
|
||||
return path.replace("+", "_").replace("/", "_").replace("~", "_")
|
||||
|
||||
def _impl(ctx):
|
||||
info = ctx.attr.binary[JavaInfo]
|
||||
|
||||
app = []
|
||||
deps = []
|
||||
seen = {}
|
||||
for jar in sorted(info.transitive_runtime_jars.to_list(), key = lambda f: f.path):
|
||||
workspace = jar.owner.workspace_name if jar.owner else ""
|
||||
bucket = "app" if workspace == "" else "deps"
|
||||
out_name = _unique_name(jar)
|
||||
|
||||
key = bucket + "/" + out_name
|
||||
if key in seen:
|
||||
fail("jar_split: duplicate output name %r for %s and %s" % (
|
||||
key,
|
||||
seen[key],
|
||||
jar.path,
|
||||
))
|
||||
seen[key] = jar.path
|
||||
|
||||
link = ctx.actions.declare_file(ctx.label.name + "/" + key)
|
||||
ctx.actions.symlink(output = link, target_file = jar)
|
||||
(app if bucket == "app" else deps).append(link)
|
||||
|
||||
return [
|
||||
DefaultInfo(files = depset(app + deps)),
|
||||
OutputGroupInfo(app = depset(app), deps = depset(deps)),
|
||||
]
|
||||
|
||||
jar_split = rule(
|
||||
implementation = _impl,
|
||||
doc = "Partitions a JVM binary's transitive runtime jars into 'app' " +
|
||||
"(first-party, empty workspace) and 'deps' (third-party) output groups.",
|
||||
attrs = {
|
||||
"binary": attr.label(
|
||||
mandatory = True,
|
||||
providers = [[JavaInfo]],
|
||||
doc = "A jvm binary/library target whose runtime classpath to split.",
|
||||
),
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user