This is an automated email from the ASF dual-hosted git repository.

leginee pushed a commit to branch bazel-migration
in repository https://gitbox.apache.org/repos/asf/openoffice.git

commit 6553ab8a6e1c43a65a125f83644fa423591c6b9d
Author: Peter Kovacs <[email protected]>
AuthorDate: Wed Aug 5 20:48:24 2026 +0200

    build(java): stage the Java2 loader runtime — classes/, jvmfwk JRE config
    
    The Java2 loader had its two DLLs (javaloader.uno, javavm.uno) but nothing
    they need at runtime.  This wires the whole bootstrap chain.
    
    The open question was how jurt/ridl/unoil reach the class loader.  There are
    TWO channels, and conflating them is what made URE_INTERNAL_JAVA_CLASSPATH
    look optional:
    
      * FIVE jars arrive by HARDCODED NAME.  javavm.cxx opens the literal
        "$URE_INTERNAL_JAVA_DIR/unoloader.jar", and UnoClassLoader.createUrls()
        then appends java_uno.jar/juh.jar/jurt.jar/ridl.jar.  No manifest, no
        classpath variable, no UNO-Type-Path: the filename IS the lookup key.
      * unoil.jar is NOT one of the five.  It reaches the loader only as the
        classPath ctor argument, i.e. $URE_INTERNAL_JAVA_CLASSPATH ->
        URE_MORE_JAVA_TYPES.  An empty value does not just lose extensions, it
        loses every com.sun.star.* office API type.
    
    UNO-Type-Path bites elsewhere: getClassLoader() reads it when javaloader 
opens
    a COMPONENT jar.  juh's empty value suppresses the hoist; an absent one 
falls
    back to "<>" = re-add the jar itself, already present via createUrls(), so a
    harmless duplicate.  Hence only juh's RegistrationClassName is reproduced —
    juh.jar is a registered component in services.rdb.
    
    New uno_jar rule (build/rules/java_pipeline.bzl, singlejar) solves the three
    build-side problems at once: exact `out =` filename at the producing target
    rather than a staging rename table; N-jar merge (ridl.jar = :ridl +
    :udkapi_java_jar — `exports` is compile-time only, so libridl.jar alone has 
no
    com.sun.star.uno.*); and manifest main attributes, which neither 
java_library
    nor jar_from_directory writes.
    
    DIVERGENCE: Sealed: is omitted.  --deploy_manifest_lines writes the main
    section only, so jurt's per-package un-sealing of com/sun/star/uno/ and
    com/sun/star/lib/util/ (packages SPLIT across jurt.jar and ridl.jar) is
    inexpressible, and a blanket Sealed:true would be STRICTER than upstream ->
    SecurityException on exactly that split.  Sealing only restricts, never
    enables, so omitting is the safe direction.
    
    Staged flat to program/classes/ via a new `flatten` option on tree_install
    (six jars from five packages, so no single strip_prefix applies).
    fundamental.ini gains URE_INTERNAL_JAVA_DIR, URE_MORE_JAVA_TYPES,
    URE_INTERNAL_JAVA_CLASSPATH and URE_OVERRIDE_JAVA_JFW_{SHARED,USER}_DATA — 
it
    is the URE_BOOTSTRAP file theMacroExpander resolves against; uno.ini is not.
    
    Two more stubs found, same family as the javaloader/javavm one (the DLL 
exists
    but the feature is compiled out):
    
      * jvmfwk.dll was built WITHOUT SOLAR_JAVA, under which framework.cxx
        compiles jfw_startVM() to a bare `return JFW_E_ERROR;`.  No 
configuration
        could ever have made Java work.  The exported ABI is unaffected — the 
JNI
        types appear only behind pointers — so consumers that include
        jvmfwk/framework.h without it still link.
      * sunjavaplugin.dll was never built.  jvmfwk holds no JRE knowledge at 
all;
        javavendors.xml maps every vendor to
        vnd.sun.star.expand:$URE_INTERNAL_LIB_DIR/sunjavaplugin.dll, which it
        osl_loadModule()s.  Built here (8 TUs, 4 exports from sunjavaplugin.map 
->
        util/sunjavaplugin.def) — a PRIVATE jvmfwk plugin interface, not a UNO
        component, so no component_getFactory and nothing in services.rdb.
        Nothing links it, so it is listed explicitly in staging.
    
    javavendors.xml (from javavendors_wnt.xml via copy_file) and jvmfwk3.ini are
    staged into program/.  The ini NAME is fixed by fwkutil.hxx's
    SAL_CONFIGFILE("/jvmfwk3") relative to the library's own directory, so it 
stays
    jvmfwk3 even though ours is jvmfwk.dll.
    
    Also fixes a pre-existing break that stopped EVERY java_library in the tree:
    //build/toolchain/java:host_jdk_runtime_toolchain does not only supply 
jar.exe,
    it also picks the JVM Bazel starts JavaBuilder on.  Having no 
target_settings
    it wins resolution unconditionally, so with JAVA_HOME a 32-bit JDK 8 every 
Java
    compile died with "Unrecognized option: --add-exports=jdk.compiler/... Could
    not create the Java Virtual Machine".  Repointed at @remotejdk21_win: 
hermetic,
    and independent of JAVA_HOME.  Java 8 bytecode is unaffected
    (--java_language_version=8 is a compiler flag, not a property of the JVM 
javac
    runs on), as is the 32-bit JVM the java_uno JNI tests host — 
java_run_test.bzl
    takes explicit jvm_path_dirs and never consults a java toolchain.
    
    Not yet exercised: nothing has loaded a real Java UNO component.  The next
    probe is cppuhelper/qa/propertysetmixin's three red testJava* cases.
    
    See main/stoc/readme.md and main/jvmfwk/readme.md.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
---
 CLAUDE.md                             |  73 ++++++++++----
 MODULE.bazel                          |  12 ++-
 build/rules/java_pipeline.bzl         |  90 +++++++++++++++++
 build/toolchain/java/BUILD.bazel      |  26 +++--
 main/bridges/BUILD.bazel              |  13 ++-
 main/javaunohelper/BUILD.bazel        |  21 ++++
 main/jurt/BUILD.bazel                 |  18 ++++
 main/jvmfwk/BUILD.bazel               |  95 ++++++++++++++++++
 main/jvmfwk/readme.md                 |  82 +++++++++++++++-
 main/jvmfwk/util/sunjavaplugin.def    |   6 ++
 main/ridljar/BUILD.bazel              |  27 +++++-
 main/staging/BUILD.bazel              |  54 +++++++++++
 main/staging/collect_files_aspect.bzl |  17 +++-
 main/staging/fundamental.ini          |  26 +++++
 main/staging/jvmfwk3.ini              |  20 ++++
 main/staging/uno.ini                  |   9 +-
 main/stoc/readme.md                   | 178 +++++++++++++++++++++-------------
 main/unoil/BUILD.bazel                |  18 +++-
 18 files changed, 682 insertions(+), 103 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 3a6d4486c2..f51b42838f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -266,27 +266,58 @@ C2027 even in javaloader.cxx which #includes "jni.h" 
ITSELF (the stub is already
 in scope); (2) javavm needs /Zc:wchar_t- (passes sal_Unicode ptr into 
NewString/
 GetStringRegion; on Windows sal_Unicode IS wchar_t, distinct from jchar unless
 wchar_t is unsigned short), javaloader does NOT (only NewStringUTF).
-NEXT STEP IS STAGING, NOT COMPILATION — and it is not just a classes/ dest:
-(a) Bazel jar names ≠ runtime names (java_library "x" → libx.jar, runtime wants
-bare unoloader.jar/jurt.jar/juh.jar/unoil.jar/java_uno.jar by EXACT string) so
-each needs an explicit destname (stage_install's manifest already has that 3rd
-column); (b) ridl.jar must be MERGED not copied — upstream's holds generated 
UDK
-types + 27 hand-written sources, but here exports=[":udkapi_java"] is
-COMPILE-time only, so libridl.jar has no com.sun.star.uno.* (fix = the
-java_binary(create_executable=False) deploy-jar trick from propertysetmixin);
-(c) the jars need MANIFESTS nothing currently emits (checked-in at
-jurt/util/manifest, javaunohelper/util/manifest, ridljar/.../unoloader/manifest
-— Sealed:, RegistrationClassName:, UNO-Type-Path:), and UnoClassLoader reads
-UNO-Type-Path off the main attributes, so they are load-bearing; neither
-jar_from_directory (bare `jar cf`) nor java_library sets one.  OPEN QUESTION to
-resolve FIRST: how jurt/ridl/unoil actually reach the class loader given
-URE_INTERNAL_JAVA_CLASSPATH expands empty and juh's UNO-Type-Path is empty —
-read UnoClassLoader.getClassLoader() + javavm.cxx's UnoClassLoader construction
-before wiring.  Also still needed: URE_INTERNAL_JAVA_DIR in
-program/fundamental.ini (NOT uno.ini — theMacroExpander resolves against the
-URE_BOOTSTRAP file; uno.ini already has it, fundamental.ini does not), and a
-jvmfwk JRE config (javavendors.xml + jvmfwk3) or jvmfwk finds no JRE.
-See main/stoc/readme.md.
+STAGING DONE 2026-08-05 — the whole bootstrap chain is now wired; what is left 
is
+to EXERCISE it (nothing has yet loaded a real Java UNO component).
+OPEN QUESTION ANSWERED — there are TWO channels, and conflating them is what 
made
+URE_INTERNAL_JAVA_CLASSPATH look optional: (1) FIVE jars arrive by HARDCODED 
NAME
+— javavm.cxx opens the literal "$URE_INTERNAL_JAVA_DIR/unoloader.jar", and
+UnoClassLoader.createUrls() then appends 
java_uno.jar/juh.jar/jurt.jar/ridl.jar.
+No manifest, no classpath var, no UNO-Type-Path involved: the FILENAME IS THE
+LOOKUP KEY.  (2) unoil.jar is NOT one of the five — it reaches the loader only 
as
+the classPath ctor arg, i.e. $URE_INTERNAL_JAVA_CLASSPATH → URE_MORE_JAVA_TYPES
+(upstream $ORIGIN/classes/unoil.jar + ScriptFramework.jar + each extension's
+UNO_JAVA_CLASSPATH).  So an empty value does not just lose extensions, it loses
+EVERY com.sun.star.* office API type.  UNO-Type-Path bites somewhere else
+entirely: getClassLoader() reads it when javaloader opens a COMPONENT jar; 
juh's
+empty value suppresses the hoist, an ABSENT one falls back to "<>" = re-add the
+jar itself, already there via createUrls() ⇒ harmless duplicate.  Hence only
+juh's RegistrationClassName was reproduced (juh.jar IS a registered component).
+NEW RULE uno_jar (build/rules/java_pipeline.bzl, singlejar) solves all three
+build-side problems at once — exact `out =` filename at the PRODUCING target
+(not a staging rename table), N-jar merge (ridl.jar = :ridl + :udkapi_java_jar,
+verified 436 classes incl. XInterface/TypeClass/UnoRuntime), and manifest main
+attributes.  DIVERGENCE: Sealed: is OMITTED.  --deploy_manifest_lines writes 
the
+MAIN section only, so jurt's per-package un-sealing of com/sun/star/uno/ +
+com/sun/star/lib/util/ (those packages are SPLIT across jurt.jar and ridl.jar) 
is
+inexpressible, and a blanket Sealed:true would be STRICTER than upstream →
+SecurityException on exactly that split.  Sealing only ever restricts, never
+enables ⇒ omitting is the safe direction.
+Staged flat to program/classes/ via //main/staging:_install_classes, using a 
new
+`flatten` opt on tree_install (six jars, five packages, no single 
strip_prefix).
+fundamental.ini got URE_INTERNAL_JAVA_DIR + URE_MORE_JAVA_TYPES +
+URE_INTERNAL_JAVA_CLASSPATH + URE_OVERRIDE_JAVA_JFW_{SHARED,USER}_DATA — it is
+the URE_BOOTSTRAP file theMacroExpander resolves against, uno.ini is NOT.
+TWO MORE STUBS FOUND, same family as the javaloader/javavm one (a DLL exists 
but
+the feature is compiled out): (a) jvmfwk.dll was built WITHOUT SOLAR_JAVA, 
under
+which framework.cxx compiles jfw_startVM() to a bare `return JFW_E_ERROR;` — no
+configuration could ever have made Java work; ABI is unaffected (JNI types 
appear
+only behind pointers) so consumers including framework.h without it still link;
+(b) sunjavaplugin.dll was NEVER BUILT — jvmfwk holds no JRE knowledge at all,
+javavendors.xml maps every vendor to
+vnd.sun.star.expand:$URE_INTERNAL_LIB_DIR/sunjavaplugin.dll which it
+osl_loadModule()s.  Now built (//main/jvmfwk, 8 TUs, 4 exports from
+sunjavaplugin.map → util/sunjavaplugin.def — a PRIVATE plugin interface, NOT a
+UNO component: no component_getFactory, nothing in services.rdb) and listed
+explicitly in staging since NOTHING LINKS IT.  javavendors.xml (from
+javavendors_wnt.xml via copy_file) + jvmfwk3.ini staged into program/; the ini
+NAME is fixed by fwkutil.hxx's SAL_CONFIGFILE("/jvmfwk3") relative to the
+library's own dir, so it stays jvmfwk3 even though ours is jvmfwk.dll.
+NOT staged: sunjavapluginrc — its one key is read via the DEFAULT 
rtl::Bootstrap,
+not a plugin-private ini, so it looks inert in an office install.
+NEXT: exercise it.  Best first probe = cppuhelper/qa/propertysetmixin's 3 RED
+testJava* cases, which need exactly this chain plus that suite's own Java
+component jar (JavaSupplier.java + .uno.jar via javamaker).
+See main/stoc/readme.md and main/jvmfwk/readme.md.
 NOTE: rules_java 8.11.0 IS now wired (MODULE.bazel) and the core Java UNO
 runtime is migrated & green — ridljar/jurt/jvmaccess/javaunohelper/jvmfwk/
 bridges (incl. the java_uno JNI bridge: java_uno.dll + java_uno.jar, done
diff --git a/MODULE.bazel b/MODULE.bazel
index dce9a9da85..3aa2f888c7 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -52,12 +52,20 @@ bazel_dep(name = "mdds",                   version = 
"0.3.1")
 bazel_dep(name = "coinmp",                 version = "1.7.6")
 bazel_dep(name = "gtest",                  version = "1.7.0")  # GoogleTest — 
C++ unit-test foundation (//main/test)
 
-# Host JDK.  Expose the machine's local JDK so we can register an ARCH-NEUTRAL 
java
+# Host JDK.  Expose a downloaded JDK so we can register an ARCH-NEUTRAL java
 # RUNTIME toolchain (see //build/toolchain/java).  rules_java only 
auto-registers
 # cpu-pinned (windows + x86_64) runtime toolchains, which stop resolving the 
moment
 # the TARGET cpu isn't x86_64 — x86_32 today, arm64/riscv as the matrix grows.
+#
+# remotejdk21_win, NOT local_jdk: this runtime is what Bazel starts 
JavaBuilder on,
+# and JavaBuilder passes --add-exports, which a JDK 8 JVM rejects outright
+# ("Unrecognized option ... Could not create the Java Virtual Machine").  
Binding
+# the build to whatever JAVA_HOME happens to be is also the exact class of
+# environment dependence this migration exists to remove.  Java 8 BYTECODE is 
still
+# guaranteed, by --java_language_version=8 in .bazelrc — a compiler flag, 
unrelated
+# to the JVM the compiler itself runs on.
 java_toolchains = use_extension("@rules_java//java:extensions.bzl", 
"toolchains")
-use_repo(java_toolchains, "local_jdk")
+use_repo(java_toolchains, "remotejdk21_win")
 
 # --------------- build-time tools (not linked, not in registry) 
---------------
 http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", 
"http_file")
diff --git a/build/rules/java_pipeline.bzl b/build/rules/java_pipeline.bzl
index 6ddbd3035e..0201090c69 100644
--- a/build/rules/java_pipeline.bzl
+++ b/build/rules/java_pipeline.bzl
@@ -207,3 +207,93 @@ no shell quoting or PATH issues arise.  The output JAR is 
named
 <name>.jar and placed in the package output directory.
 """,
 )
+
+# ---------------------------------------------------------------------------
+# uno_jar
+# ---------------------------------------------------------------------------
+# Produce a jar under the EXACT filename the UNO runtime looks it up by.
+#
+# Three problems, one rule:
+#
+#  (a) Bazel jar names are not runtime jar names.  java_library(name = "x")
+#      emits libx.jar, but com/sun/star/lib/unoloader/UnoClassLoader.java
+#      hardcodes "java_uno.jar" / "juh.jar" / "jurt.jar" / "ridl.jar" and
+#      stoc/source/javavm/javavm.cxx hardcodes "unoloader.jar".  These are
+#      exact-string lookups relative to $URE_INTERNAL_JAVA_DIR, so the staged
+#      basename is load-bearing.  `out =` names the output directly rather
+#      than renaming at staging time, so the name lives next to the target
+#      that produces it.
+#
+#  (b) Some runtime jars are the union of several Bazel targets.  ridl.jar
+#      upstream holds the javamaker-generated UDK types AND the hand-written
+#      ridl sources; here those are two targets and java_library's `exports`
+#      is COMPILE-time only, so libridl.jar alone has no com.sun.star.uno.*.
+#      `jars =` takes any number of inputs and merges them.
+#
+#  (c) The manifest main attributes are read at runtime.  javaloader reads
+#      RegistrationClassName to register a Java component; UnoClassLoader
+#      .getClassLoader() reads UNO-Type-Path.  Neither java_library nor
+#      jar_from_directory (a bare `jar cf`) writes a manifest.
+#
+# singlejar does all three, and is already part of the Bazel JDK tooling —
+# no new external dependency.
+# ---------------------------------------------------------------------------
+
+def _uno_jar_impl(ctx):
+    jar_out = ctx.actions.declare_file(ctx.attr.out)
+
+    srcs = []
+    for t in ctx.attr.jars:
+        srcs += [f for f in t.files.to_list() if f.extension == "jar"]
+    if not srcs:
+        fail("uno_jar '%s': jars = produced no .jar files" % ctx.label.name)
+
+    args = ctx.actions.args()
+    args.add("--output", jar_out)
+    args.add("--sources")
+    args.add_all(srcs)
+    if ctx.attr.manifest_lines:
+        args.add("--deploy_manifest_lines")
+        args.add_all(ctx.attr.manifest_lines)
+    # Deterministic output: no timestamps, no build-data.properties stamp.
+    args.add("--normalize")
+    args.add("--exclude_build_data")
+
+    ctx.actions.run(
+        executable       = ctx.executable._singlejar,
+        arguments        = [args],
+        inputs           = depset(srcs),
+        outputs          = [jar_out],
+        mnemonic         = "UnoJar",
+        progress_message = "Assembling %s" % jar_out.basename,
+    )
+
+    return [DefaultInfo(files = depset([jar_out]))]
+
+uno_jar = rule(
+    implementation = _uno_jar_impl,
+    attrs = {
+        "out": attr.string(
+            mandatory = True,
+            doc       = "Exact output filename, e.g. \"ridl.jar\" — the name 
the UNO runtime looks up.",
+        ),
+        "jars": attr.label_list(
+            allow_files = True,
+            mandatory   = True,
+            doc         = "Jars to merge, in order.  A single entry simply 
restamps name/manifest.",
+        ),
+        "manifest_lines": attr.string_list(
+            doc = "Manifest MAIN-section attributes, e.g. 
[\"RegistrationClassName: com.sun.star.comp.Foo\"].",
+        ),
+        "_singlejar": attr.label(
+            default    = "@bazel_tools//tools/jdk:singlejar",
+            executable = True,
+            cfg        = "exec",
+        ),
+    },
+    doc = """
+Assemble a runtime UNO jar: merge jars, stamp manifest main attributes, and
+emit under an exact filename.  See the block comment above for why each of
+the three is needed.
+""",
+)
diff --git a/build/toolchain/java/BUILD.bazel b/build/toolchain/java/BUILD.bazel
index bfd627b4f3..a3ddefa208 100644
--- a/build/toolchain/java/BUILD.bazel
+++ b/build/toolchain/java/BUILD.bazel
@@ -17,8 +17,9 @@
 #
 # The fix — and why it is arch-neutral
 # ------------------------------------
-# This runtime toolchain is only ever used as a HOST BUILD TOOL: jar.exe 
zipping
-# arch-neutral .class files (see build/rules/java_pipeline.bzl 
jar_from_directory).
+# This runtime toolchain is only ever used as a HOST BUILD TOOL: the JVM Bazel
+# starts JavaBuilder on, jar.exe zipping arch-neutral .class files (see
+# build/rules/java_pipeline.bzl jar_from_directory), and singlejar (uno_jar).
 # javamaker itself runs as a native exec-cfg cc_binary, NOT through this 
toolchain.
 # So a single host JDK serves EVERY target arch.  We therefore register ONE 
runtime
 # toolchain with no cpu constraint; it matches any windows target platform, 
present
@@ -26,13 +27,26 @@
 #
 # It is registered in the ROOT module (MODULE.bazel), so it outranks 
rules_java's
 # cpu-pinned remote-JDK runtime toolchains and is selected for both x86 and 
x64.
+# That priority is total — it has no target_settings, so 
--java_runtime_version and
+# --tool_java_runtime_version cannot steer around it.  Which makes WHICH jdk 
this
+# points at load-bearing for every Java action in the build:
+#
+#   NOT @local_jdk.  JavaBuilder is launched with --add-exports, which a JDK 8 
JVM
+#   rejects before main() ("Unrecognized option ... Could not create the Java
+#   Virtual Machine") — and this machine's JAVA_HOME is a 32-bit JDK 8, so 
every
+#   java_library in the tree failed to compile.  Java 8 BYTECODE does not 
require a
+#   Java 8 javac: --java_language_version=8 in .bazelrc pins the class-file 
level.
+#
+# The 32-bit Java 8 JVM that the java_uno JNI bridge tests host is NOT 
affected:
+# build/rules/java_run_test.bzl takes explicit jvm_path_dirs and never 
consults a
+# java toolchain.
 
 toolchain(
     name = "host_jdk_runtime_toolchain",
-    # The machine's local JDK (JAVA_HOME), wired via the rules_java 
`toolchains`
-    # module extension in MODULE.bazel.  It runs natively on the x64 build host
-    # regardless of the TARGET arch of the product being built.
-    toolchain = "@local_jdk//:jdk",
+    # Downloaded JDK 21, wired via the rules_java `toolchains` module 
extension in
+    # MODULE.bazel.  Hermetic — independent of JAVA_HOME — and runs natively 
on the
+    # x64 build host regardless of the TARGET arch of the product being built.
+    toolchain = "@remotejdk21_win//:jdk",
     toolchain_type = "@bazel_tools//tools/jdk:runtime_toolchain_type",
     # No cpu constraint: arch-neutral by design (see header).  os:windows only,
     # so it matches every windows target platform in the matrix.
diff --git a/main/bridges/BUILD.bazel b/main/bridges/BUILD.bazel
index 1c98408b21..fcb56343bb 100644
--- a/main/bridges/BUILD.bazel
+++ b/main/bridges/BUILD.bazel
@@ -4,7 +4,7 @@ load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library")
 load("@rules_java//java:defs.bzl", "java_library", "java_import")
 load("//build/rules:gtest_test.bzl", "gtest_test", "staged_run_test")
 load("//build/rules:idl_pipeline.bzl", "idl_library")
-load("//build/rules:java_pipeline.bzl", "javamaker_classes", 
"jar_from_directory")
+load("//build/rules:java_pipeline.bzl", "javamaker_classes", 
"jar_from_directory", "uno_jar")
 load("//build/rules:native_lib_dir.bzl", "native_lib_dir")
 load("//build/rules:java_run_test.bzl", "staged_java_test")
 
@@ -287,6 +287,17 @@ java_library(
     visibility = ["//visibility:public"],
 )
 
+# java_uno.jar → program/classes/.  First of the four names hardcoded in
+# UnoClassLoader.createUrls(), so the basename is load-bearing (the comment
+# above about the sealed manifest being "handled at staging" is now here:
+# Sealed is omitted, for the same reason as jurt.jar — see //main/jurt).
+uno_jar(
+    name       = "java_uno_runtime_jar",
+    out        = "java_uno.jar",
+    jars       = [":java_uno_jar"],
+    visibility = ["//visibility:public"],
+)
+
 # ═══════════════════════════════════════════════════════════════════════════
 # Tests
 # ═══════════════════════════════════════════════════════════════════════════
diff --git a/main/javaunohelper/BUILD.bazel b/main/javaunohelper/BUILD.bazel
index 2281c9ec49..139e4dd5fb 100644
--- a/main/javaunohelper/BUILD.bazel
+++ b/main/javaunohelper/BUILD.bazel
@@ -1,4 +1,5 @@
 load("@rules_cc//cc:defs.bzl", "cc_binary")
+load("//build/rules:java_pipeline.bzl", "uno_jar")
 load("@rules_java//java:defs.bzl", "java_library")
 
 exports_files(["util/juh.component"])
@@ -33,6 +34,26 @@ java_library(
     visibility = ["//visibility:public"],
 )
 
+# ── juh.jar → program/classes/ 
────────────────────────────────────────────────
+# Two exact-string lookups land on this file: UnoClassLoader.createUrls() names
+# it "juh.jar", and services.rdb registers util/juh.component at
+# vnd.sun.star.expand:$URE_INTERNAL_JAVA_DIR/juh.jar (see //main/postprocess).
+#
+# Because it IS a registered component, RegistrationClassName is load-bearing —
+# javaloader reads it off the main attributes to find the class to register.
+# Upstream's util/manifest also carries "UNO-Type-Path:" with a deliberately
+# EMPTY value; that is omitted here because an absent UNO-Type-Path makes
+# UnoClassLoader.getClassLoader() fall back to "<>", i.e. add juh.jar itself to
+# the root loader — which createUrls() has already added, so the fallback is a
+# harmless duplicate rather than a behaviour change.
+uno_jar(
+    name           = "juh_runtime_jar",
+    out            = "juh.jar",
+    jars           = [":juh_jar"],
+    manifest_lines = ["RegistrationClassName: 
com.sun.star.comp.JavaUNOHelperServices"],
+    visibility     = ["//visibility:public"],
+)
+
 # ── juh.dll (preload) 
──────────────────────────────────────────────────────────
 # Thin DLL loaded by Java via System.loadLibrary("juh"). On first JNI call it
 # lazily loads juhx.dll via osl_loadModuleRelative and forwards all 4 JNI
diff --git a/main/jurt/BUILD.bazel b/main/jurt/BUILD.bazel
index ef1af69bc2..74b15e20f1 100644
--- a/main/jurt/BUILD.bazel
+++ b/main/jurt/BUILD.bazel
@@ -1,3 +1,4 @@
+load("//build/rules:java_pipeline.bzl", "uno_jar")
 load("@rules_cc//cc:defs.bzl", "cc_binary")
 load("@rules_java//java:defs.bzl", "java_library")
 
@@ -29,6 +30,23 @@ java_library(
     visibility = ["//visibility:public"],
 )
 
+# ── jurt.jar → program/classes/ 
───────────────────────────────────────────────
+# UnoClassLoader.createUrls() looks this up as the bare string "jurt.jar".
+#
+# DIVERGENCE (deliberate): upstream's util/manifest seals the jar but un-seals
+# com/sun/star/lib/util/ and com/sun/star/uno/, because those two packages are
+# split across jurt.jar and ridl.jar.  A per-package manifest section cannot be
+# expressed through singlejar's --deploy_manifest_lines (main section only), 
and
+# a blanket "Sealed: true" here would be STRICTER than upstream and throw
+# SecurityException on exactly that split.  Omitting Sealed is the safe
+# direction — sealing only ever restricts, never enables.
+uno_jar(
+    name       = "jurt_jar",
+    out        = "jurt.jar",
+    jars       = [":jurt"],
+    visibility = ["//visibility:public"],
+)
+
 # ── jpipx.dll 
─────────────────────────────────────────────────────────────────
 # Real JNI pipe implementation.  On Windows, functions are exported with
 # truncated names (PipeConnection_create etc., __cdecl) so wrapper.c can call
diff --git a/main/jvmfwk/BUILD.bazel b/main/jvmfwk/BUILD.bazel
index 7a3f29ba91..5ff52ae393 100644
--- a/main/jvmfwk/BUILD.bazel
+++ b/main/jvmfwk/BUILD.bazel
@@ -1,5 +1,6 @@
 package(default_visibility = ["//visibility:public"])
 
+load("//build/rules:copy_file.bzl", "copy_file")
 load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library")
 
 _COPTS = [
@@ -20,16 +21,35 @@ _DEFINES = [
     "FULL_DESK",
     "SHAREDLIB", "_DLL_",
     "JVMFWK_DLLIMPLEMENTATION",
+    # SOLAR_JAVA is not a feature flag here, it is the difference between a
+    # working library and a stub.  jvmfwk/source/framework.cxx compiles
+    # jfw_startVM() to a bare `return JFW_E_ERROR;` under #ifndef SOLAR_JAVA
+    # (and jvmfwk/framework.h then declares JavaVM/JNIEnv/JavaVMOption as
+    # incomplete types instead of including <jni.h>).  Built without it, every
+    # caller — stoc's javavm.uno, cui's Java options page, javaldx — gets a
+    # generic error no matter how correct the JRE, javavendors.xml and
+    # jvmfwk3.ini are.  Upstream drives this from configure; here it is
+    # unconditional, as rules_java is a hard dependency (same reasoning as
+    # //main/stoc's javaloader/javavm).
+    #
+    # The exported ABI does not change: the affected types appear only behind
+    # pointers, so consumers that include jvmfwk/framework.h WITHOUT SOLAR_JAVA
+    # (desktop, cui, svl…) still link and call correctly.
+    "SOLAR_JAVA",
 ]
 
 _DEPS = [
     "//main/sal:sal_headers",
+    # salhelper/simplereferenceobject.hxx — VendorBase's base class.
+    "//main/salhelper:salhelper_headers",
     "//main/cppu:cppu_headers",
     "//main/cppuhelper:cppuhelper_headers",
     "//main/udkapi:udkapi_idl_headers",
     "//main/stlport:stlport",
     "@boost.legacy//:boost.legacy",
     "@libxml2//:libxml2",
+    # <jni.h>, reached through jvmfwk/framework.h once SOLAR_JAVA is on.
+    "@rules_java//toolchains:jni",
 ]
 
 # ── jvmfwk headers (for downstream consumers) ────────────────────
@@ -81,6 +101,65 @@ cc_binary(
     visibility = ["//visibility:public"],
 )
 
+# ── sunjavaplugin.dll ────────────────────────────────────────────
+# The vendor plugin: given a candidate JRE it reports vendor/version/runtime
+# lib, and it is what actually calls JNI_CreateJavaVM.  jvmfwk itself contains
+# no JRE knowledge — javavendors.xml maps every supported vendor to
+# "vnd.sun.star.expand:$URE_INTERNAL_LIB_DIR/sunjavaplugin.dll", which jvmfwk
+# osl_loadModule()s.  Without it jfw_findAllJREs finds nothing and the office
+# reports no Java however many JDKs are installed.
+#
+# The four exports come from plugins/sunmajor/pluginlib/sunjavaplugin.map,
+# converted to util/sunjavaplugin.def (the standard .map → .def conversion; 
note
+# these are NOT the UNO component entry points — this is a private jvmfwk 
plugin
+# interface, not a registered UNO component, so there is no
+# component_getFactory here and it does not belong in services.rdb).
+cc_binary(
+    name = "sunjavaplugin",
+    srcs = [
+        "plugins/sunmajor/pluginlib/gnujre.cxx",
+        "plugins/sunmajor/pluginlib/gnujre.hxx",
+        "plugins/sunmajor/pluginlib/otherjre.cxx",
+        "plugins/sunmajor/pluginlib/otherjre.hxx",
+        "plugins/sunmajor/pluginlib/sunjavaplugin.cxx",
+        "plugins/sunmajor/pluginlib/sunjre.cxx",
+        "plugins/sunmajor/pluginlib/sunjre.hxx",
+        "plugins/sunmajor/pluginlib/sunversion.cxx",
+        "plugins/sunmajor/pluginlib/sunversion.hxx",
+        "plugins/sunmajor/pluginlib/util.cxx",
+        "plugins/sunmajor/pluginlib/util.hxx",
+        "plugins/sunmajor/pluginlib/vendorbase.cxx",
+        "plugins/sunmajor/pluginlib/vendorbase.hxx",
+        "plugins/sunmajor/pluginlib/vendorlist.cxx",
+        "plugins/sunmajor/pluginlib/vendorlist.hxx",
+        "plugins/sunmajor/pluginlib/diagnostics.h",
+        # pch, included by every .cxx as "precompiled_plugin.hxx"
+        "inc/pch/precompiled_plugin.hxx",
+    ],
+    copts = _COPTS,
+    defines = _DEFINES,
+    deps = _DEPS,
+    additional_linker_inputs = [
+        "//main/sal:sal_implib",
+        "//main/salhelper:salhelper_implib",
+        "//main/cppu:cppu3_implib",
+        "//main/cppuhelper:cppuhelper_implib",
+    ],
+    linkshared = True,
+    win_def_file = "util/sunjavaplugin.def",
+    linkopts = [
+        "$(execpath //main/sal:sal_implib)",
+        "$(execpath //main/salhelper:salhelper_implib)",
+        "$(execpath //main/cppu:cppu3_implib)",
+        "$(execpath //main/cppuhelper:cppuhelper_implib)",
+        # util.cxx reads HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft to enumerate
+        # installed JREs.
+        "advapi32.lib",
+        "/MANIFEST:NO",
+    ],
+    visibility = ["//visibility:public"],
+)
+
 # ── import library for downstream DLLs ───────────────────────────
 filegroup(
     name = "jvmfwk_implib",
@@ -91,3 +170,19 @@ filegroup(
 
 # Alias so existing references still resolve during transition.
 alias(name = "jvmfwk3_implib", actual = ":jvmfwk_implib", visibility = 
["//visibility:public"])
+
+# ── javavendors.xml → program/ ───────────────────────────────────────────────
+# The JRE-selection policy: which vendors are acceptable, at which minimum
+# version, and which plugin library can inspect a candidate JRE for each.
+# jvmfwk finds it through UNO_JAVA_JFW_VENDOR_SETTINGS in program/jvmfwk3.ini;
+# VendorSettings' constructor throws JFW_E_CONFIGURATION if it is absent, so no
+# JVM is located at all without this file.
+#
+# Renamed, not just staged: upstream picks one of the per-platform variants
+# (scp2 ure.scp gid_File_Misc_JavavendorsXml installs it under the bare name)
+# and the bootstrap value names javavendors.xml exactly.
+copy_file(
+    name = "javavendors_xml",
+    src  = "distributions/OpenOfficeorg/javavendors_wnt.xml",
+    out  = "javavendors.xml",
+)
diff --git a/main/jvmfwk/readme.md b/main/jvmfwk/readme.md
index cce108c4b7..4baf6c284c 100644
--- a/main/jvmfwk/readme.md
+++ b/main/jvmfwk/readme.md
@@ -1,10 +1,88 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you 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.
+-->
+
 # Notes for jvmfwk (done)
 
-- Single DLL: `jvmfwk.dll`
-- Deps: cppu, cppuhelper, sal, udkapi_idl_headers, stlport, boost.legacy, 
@libxml2//:libxml2, advapi32
+- Two DLLs: `jvmfwk.dll` and `sunjavaplugin.dll`
+- Deps: cppu, cppuhelper, sal, salhelper, udkapi_idl_headers, stlport, 
boost.legacy, @libxml2//:libxml2, @rules_java//toolchains:jni, advapi32
 - Exports: 21 C-linkage `jfw_*` functions from `inc/jvmfwk/` via `jvmfwk.def`
 
+## `SOLAR_JAVA` is the difference between a library and a stub
+
+`jvmfwk.dll` was originally built without it. That is not a feature switch:
+`source/framework.cxx` compiles `jfw_startVM()` to a bare
+`return JFW_E_ERROR;` under `#ifndef SOLAR_JAVA`, and `inc/jvmfwk/framework.h`
+then declares `JavaVM` / `JNIEnv` / `JavaVMOption` as incomplete types instead 
of
+including `<jni.h>`. Built that way, no configuration can make Java work —
+stoc's `javavm.uno`, cui's Java options page and `javaldx` all get a generic
+error however correct the JRE, `javavendors.xml` and `jvmfwk3.ini` are.
+
+Upstream drives it from `configure`; here it is unconditional, since 
`rules_java`
+is a hard dependency. It brings in `@rules_java//toolchains:jni` for the real
+`<jni.h>`. The exported ABI is unaffected — those types appear only behind
+pointers, so consumers that include `jvmfwk/framework.h` *without* `SOLAR_JAVA`
+(desktop, cui, svl, svtools) still compile and link against the same entry
+points.
+
+Same landmine, third occurrence: see `//main/bridges` (java_uno JNI bridge) and
+`//main/stoc` (javaloader/javavm).
+
+## `sunjavaplugin.dll` — the half that knows about JREs
+
+`jvmfwk` itself contains no JRE knowledge at all. `javavendors.xml` maps each
+supported vendor to
+`vnd.sun.star.expand:$URE_INTERNAL_LIB_DIR/sunjavaplugin.dll`, which jvmfwk
+`osl_loadModule()`s; the plugin enumerates candidate JREs (registry
+`HKLM\SOFTWARE\JavaSoft` — hence `advapi32`), reports vendor/version/runtime 
lib,
+and is what actually calls `JNI_CreateJavaVM`.
+
+- Sources: the 8 TUs of `plugins/sunmajor/pluginlib` (pch
+  `precompiled_plugin.hxx`, not `precompiled_jvmfwk.hxx`).
+- Exports: 4 functions converted from 
`plugins/sunmajor/pluginlib/sunjavaplugin.map`
+  to `util/sunjavaplugin.def`. These are a **private jvmfwk plugin interface, 
not
+  UNO component entry points** — no `component_getFactory`, and nothing in
+  `services.rdb`.
+- **Nothing links it**, so it must be listed explicitly in
+  `//main/staging` (same class of runtime-only dependency as the C++/UNO bridge
+  DLL). Missing, the office reports no Java installed however many JDKs are 
present.
+- No `/Zc:wchar_t-`: unlike `javavm`, this code never hands `sal_Unicode*` to a
+  JNI string call.
+
+## Runtime configuration staged into `program/`
+
+- `javavendors.xml` — from `distributions/OpenOfficeorg/javavendors_wnt.xml`, 
via
+  `copy_file` because the bootstrap value names the bare filename.
+- `jvmfwk3.ini` — in `main/staging/`. The name is fixed by
+  `source/fwkutil.hxx`, which hardcodes `SAL_CONFIGFILE("/jvmfwk3")` relative 
to
+  its own library directory, so it stays `jvmfwk3` even though the library here
+  is `jvmfwk.dll` rather than upstream's `jvmfwk3.dll`.
+
+Not staged: `plugins/sunmajor/pluginlib/sunjavapluginrc`. Its one key
+(`JFW_PLUGIN_DO_NOT_CHECK_ACCESSIBILITY`) is read through the *default*
+`rtl::Bootstrap`, not a plugin-private ini, so the file looks inert in an 
office
+install; without it jvmfwk falls back to probing
+`HKCU\Software\OpenOffice\Accessibility\AtToolSupport`.
+
+See `main/stoc/readme.md` for the full Java2-loader bootstrap chain.
+
 ## libxml2 overlay fixes for VS2008
+
 - `config.h` overlay: win32/VC10/config.h as base, HAVE_STDINT_H guarded to 
VS2010+, SEND_ARG2_CAST defined empty
 - `libxml2-configure.patch` disables FTP, HTTP, iconv, debug, zlib, lzma in 
xmlversion.h
 - nanoftp.c and nanohttp.c excluded from LIBXML2_SRCS (networking not needed)
diff --git a/main/jvmfwk/util/sunjavaplugin.def 
b/main/jvmfwk/util/sunjavaplugin.def
new file mode 100644
index 0000000000..aafc97c4ba
--- /dev/null
+++ b/main/jvmfwk/util/sunjavaplugin.def
@@ -0,0 +1,6 @@
+LIBRARY sunjavaplugin
+EXPORTS
+    jfw_plugin_getAllJavaInfos
+    jfw_plugin_getJavaInfoByPath
+    jfw_plugin_startJavaVirtualMachine
+    jfw_plugin_existJRE
diff --git a/main/ridljar/BUILD.bazel b/main/ridljar/BUILD.bazel
index ead3ce93fd..ccc89cac57 100644
--- a/main/ridljar/BUILD.bazel
+++ b/main/ridljar/BUILD.bazel
@@ -1,4 +1,4 @@
-load("//build/rules:java_pipeline.bzl", "jar_from_directory", 
"javamaker_classes")
+load("//build/rules:java_pipeline.bzl", "jar_from_directory", 
"javamaker_classes", "uno_jar")
 load("@rules_java//java:defs.bzl", "java_import", "java_library")
 
 # ── Step 1: Generate Java .class files from udkapi.rdb ───────────────────
@@ -46,3 +46,28 @@ java_library(
     srcs       = glob(["java/unoloader/src/main/java/**/*.java"]),
     visibility = ["//visibility:public"],
 )
+
+# ── Runtime jars → program/classes/ ──────────────────────────────────────
+# Staged by //main/staging:install.  The names are exact-string lookups, not
+# conventions: javavm.cxx::setUpUnoVirtualMachine builds a URLClassLoader over
+# "$URE_INTERNAL_JAVA_DIR/unoloader.jar" to reach UnoClassLoader, whose
+# createUrls() then hardcodes java_uno.jar / juh.jar / jurt.jar / ridl.jar
+# relative to the same base.  See main/stoc/readme.md.
+
+uno_jar(
+    name       = "unoloader_jar",
+    out        = "unoloader.jar",
+    jars       = [":unoloader"],
+    visibility = ["//visibility:public"],
+)
+
+# ridl.jar is a MERGE, not a rename.  Upstream's single jar holds both the
+# javamaker-generated UDK types and the 27 hand-written sources; here they are
+# two targets and `exports` above only wires them together at COMPILE time, so
+# libridl.jar on its own contains no com.sun.star.uno.*.
+uno_jar(
+    name       = "ridl_jar",
+    out        = "ridl.jar",
+    jars       = [":ridl", ":udkapi_java_jar"],
+    visibility = ["//visibility:public"],
+)
diff --git a/main/staging/BUILD.bazel b/main/staging/BUILD.bazel
index 02fb4fa870..949733cc3a 100644
--- a/main/staging/BUILD.bazel
+++ b/main/staging/BUILD.bazel
@@ -11,6 +11,9 @@ filegroup(
         "uno.ini",
         # PYUNO_LOADER_PYTHONHOME / PYTHONPATH for pythonloader.dll.
         "pythonloader.uno.ini",
+        # Vendor settings + JRE-selection data paths for jvmfwk.dll.  Name 
fixed
+        # by fwkutil.hxx, not by us — see the file's own header.
+        "jvmfwk3.ini",
     ],
 )
 
@@ -66,6 +69,12 @@ collect_outputs(
 
         # ── JVM framework (required by cui, svl, svtools, desktop at load 
time) ─
         "//main/jvmfwk:jvmfwk",
+        # Vendor plugin — the half that actually knows how to find and start a
+        # JRE.  jvmfwk osl_loadModule()s it by the path in javavendors.xml
+        # ("vnd.sun.star.expand:$URE_INTERNAL_LIB_DIR/sunjavaplugin.dll"), so
+        # nothing links it and it must be listed here explicitly.  Missing, the
+        # office reports no Java installed however many JDKs are present.
+        "//main/jvmfwk:sunjavaplugin",
 
         # ── UNO runtime infrastructure 
────────────────────────────────────────
         "//main/stoc:bootstrap.uno",
@@ -620,6 +629,49 @@ tree_install(
     dst          = "share/config",
 )
 
+# ── program/classes/ — Java UNO runtime jars ($URE_INTERNAL_JAVA_DIR) ────────
+# com.sun.star.loader.Java2 bootstraps from here.  javavm.cxx builds a
+# URLClassLoader over "$URE_INTERNAL_JAVA_DIR/unoloader.jar" to reach
+# com.sun.star.lib.unoloader.UnoClassLoader, whose createUrls() then hardcodes
+# java_uno.jar / juh.jar / jurt.jar / ridl.jar relative to the same base.  All
+# five are exact-string lookups, which is why the uno_jar rule fixes each 
output
+# filename at the producing target rather than renaming during staging.
+#
+# unoil.jar is the exception: it is NOT one of the hardcoded names and arrives
+# through the second channel — $URE_INTERNAL_JAVA_CLASSPATH → 
URE_MORE_JAVA_TYPES
+# in program/fundamental.ini, passed to UnoClassLoader as its classPath 
argument.
+#
+# flatten: the six jars come from five different packages, so no single
+# strip_prefix applies.
+filegroup(
+    name = "java_runtime_jars",
+    srcs = [
+        "//main/ridljar:unoloader_jar",
+        "//main/ridljar:ridl_jar",
+        "//main/jurt:jurt_jar",
+        "//main/javaunohelper:juh_runtime_jar",
+        "//main/bridges:java_uno_runtime_jar",
+        "//main/unoil:unoil_jar",
+    ],
+)
+
+tree_install(
+    name    = "_install_classes",
+    src     = ":java_runtime_jars",
+    flatten = True,
+    dst     = "program/classes",
+)
+
+# ── program/javavendors.xml — jvmfwk JRE-selection policy ────────────────────
+# Not staged by _install_program: flat_install filters on _INSTALL_EXTS, and
+# adding "xml" there would sweep in every .xml in the transitive closure.
+tree_install(
+    name    = "_install_java_config",
+    src     = "//main/jvmfwk:javavendors_xml",
+    flatten = True,
+    dst     = "program",
+)
+
 # ── share/config/soffice.cfg — per-module UI configuration tree 
───────────────
 # The framework PresetHandler opens share/config/soffice.cfg as a *folder* and
 # reads modules/<ModuleShortName>/{menubar,toolbar,statusbar}/*.xml at startup
@@ -657,6 +709,8 @@ filegroup(
         ":_install_images_zip",
         ":_install_images_industrial_zip",
         ":_install_uiconfig",
+        ":_install_classes",
+        ":_install_java_config",
         ":_install_python_scripts",
         # The Python stdlib tree is NOT optional: pythonloader.dll calls
         # Py_Initialize(), and Python 2.7 exit()s the process if it cannot find
diff --git a/main/staging/collect_files_aspect.bzl 
b/main/staging/collect_files_aspect.bzl
index 0164b5bb9a..23f222c10b 100644
--- a/main/staging/collect_files_aspect.bzl
+++ b/main/staging/collect_files_aspect.bzl
@@ -173,7 +173,11 @@ flat_install = rule(
 # strip_prefix: path component within the repo to strip.
 #   For "@python//:python_lib" (glob(["Lib/**"])), set strip_prefix = "Lib"
 #   so "external/python+/Lib/abc.py" → "abc.py" → installed at dst/abc.py.
-#   Set "" to strip nothing (files land at dst/<basename>).
+#   Set "" to strip nothing (files keep their full in-repo path under dst).
+#
+# flatten: discard the tree entirely — every file lands at dst/<basename>.
+#   Use when the sources come from several packages, so no single strip_prefix
+#   applies, and the tree carries no meaning at the destination.
 #
 # dst: destination subdirectory inside the rule's output dir.
 #   e.g. "python-core-2.7.18/lib"
@@ -213,7 +217,15 @@ def _tree_install_impl(ctx):
             in_repo = f.short_path
 
         # Strip the known prefix (e.g. "Lib/") to get the bare relative path.
-        if strip:
+        if ctx.attr.flatten:
+            # No tree to preserve: every file lands directly in dst under its 
own
+            # basename.  For inputs that come from SEVERAL packages there is no
+            # single strip_prefix that works (each would need its own), and the
+            # destination name is already fixed by the producing rule — as for 
the
+            # Java UNO runtime jars, whose basenames are exact-string lookups 
in
+            # javavm.cxx / UnoClassLoader (see build/rules/java_pipeline.bzl).
+            rel = f.basename
+        elif strip:
             marker = strip + "/"
             idx = in_repo.find(marker)
             if idx >= 0:
@@ -265,6 +277,7 @@ tree_install = rule(
     attrs = {
         "src":          attr.label(allow_files = True),
         "strip_prefix": attr.string(default = "", doc = "In-repo path prefix 
to strip (e.g. 'Lib')"),
+        "flatten":      attr.bool(default = False, doc = "Ignore strip_prefix 
and install every file flat into dst under its basename"),
         "dst":          attr.string(mandatory = True, doc = "Destination 
subdir in output (e.g. 'python-core-2.7.18/lib')"),
     },
 )
diff --git a/main/staging/fundamental.ini b/main/staging/fundamental.ini
index 95e80d6f43..cb077861eb 100644
--- a/main/staging/fundamental.ini
+++ b/main/staging/fundamental.ini
@@ -10,6 +10,32 @@ UNO_SERVICES=${ORIGIN}/services.rdb
 URE_MORE_TYPES=${ORIGIN}/oovbaapi.rdb
 URE_MORE_SERVICES=
 URE_MORE_JAVA_CLASSPATH_URLS=
+# ── Java (com.sun.star.loader.Java2) ─────────────────────────────────────────
+# These MUST live here and not only in uno.ini.  stoc/source/javavm/javavm.cxx
+# resolves them through com.sun.star.util.theMacroExpander, which expands 
against
+# the URE_BOOTSTRAP file — i.e. THIS one.  Same trap as the extension path 
macros
+# below.  getJavaVM() raises a RuntimeException if URE_INTERNAL_JAVA_DIR is not
+# expandable; URE_INTERNAL_JAVA_CLASSPATH is explicitly allowed not to be.
+#
+# URE_INTERNAL_JAVA_DIR is the base for the FIVE jars looked up by hardcoded
+# name: javavm.cxx itself opens unoloader.jar, and the UnoClassLoader it then
+# instantiates appends java_uno.jar / juh.jar / jurt.jar / ridl.jar in
+# createUrls().  Staged by //main/staging:_install_classes.
+URE_INTERNAL_JAVA_DIR=${ORIGIN}/classes
+# unoil.jar (the office API types) is NOT one of those five.  It reaches the
+# class loader only as the classPath argument, i.e. via this variable — which 
is
+# why leaving it empty silently costs every com.sun.star.* API type.  Upstream
+# also lists ScriptFramework.jar plus each extension's UNO_JAVA_CLASSPATH here
+# (scp2 profileitem_ooo.scp 
gid_Profileitem_Fundamentalbasis_Ure_More_Java_Types);
+# neither is built yet.
+URE_MORE_JAVA_TYPES=${ORIGIN}/classes/unoil.jar
+URE_INTERNAL_JAVA_CLASSPATH=${URE_MORE_JAVA_TYPES}
+# Where jvmfwk records which JRE was selected.  Read by program/jvmfwk3.ini,
+# which is the bootstrap file jvmfwk loads from its OWN directory
+# (fwkutil.hxx hardcodes SAL_CONFIGFILE("/jvmfwk3"), so the name stays jvmfwk3
+# even though the library is jvmfwk.dll here).
+URE_OVERRIDE_JAVA_JFW_SHARED_DATA=${OOO_BASE_DIR}/share/config/javasettings_${_OS}_${_ARCH}.xml
+URE_OVERRIDE_JAVA_JFW_USER_DATA=${${OOO_BASE_DIR}/program/bootstrap.ini:UserInstallation}/user/config/javasettings_${_OS}_${_ARCH}.xml
 # Extension / deployment path macros.  These are also defined in uno.ini, but
 # the UNO component-context bootstrap (which reads unorc/uno.ini) is NOT the
 # context that rtl::Bootstrap::expandMacros() resolves against -- that uses the
diff --git a/main/staging/jvmfwk3.ini b/main/staging/jvmfwk3.ini
new file mode 100644
index 0000000000..b532302952
--- /dev/null
+++ b/main/staging/jvmfwk3.ini
@@ -0,0 +1,20 @@
+[Bootstrap]
+# jvmfwk's own bootstrap file.  The NAME is not ours to choose: jvmfwk loads it
+# from the directory of its own library via SAL_CONFIGFILE("/jvmfwk3") 
hardcoded
+# in jvmfwk/source/fwkutil.hxx, so it stays jvmfwk3.ini even though the library
+# is built as jvmfwk.dll here (see //main/jvmfwk).
+#
+# ${ORIGIN} is program/, where //main/staging:_install_java_config puts
+# javavendors.xml.  Without a readable vendor settings file, VendorSettings'
+# constructor throws JFW_E_CONFIGURATION and no JVM is ever located.
+UNO_JAVA_JFW_VENDOR_SETTINGS=${ORIGIN}/javavendors.xml
+# The URE_OVERRIDE_* macros are defined in fundamental.ini, not here; a plain
+# ${NAME} falls back to the base bootstrap ini, which is how upstream's URE
+# jvmfwk3rc reaches the same values in fundamentalbasisrc.  Each is a
+# space-separated list and jvmfwk takes the FIRST URL (getParamFirstUrl), so 
the
+# per-user path is a fallback for when the install dir is not writable.
+UNO_JAVA_JFW_SHARED_DATA=${URE_OVERRIDE_JAVA_JFW_SHARED_DATA} 
${SYSUSERCONFIG}/URE/javasettings_${_OS}_${_ARCH}.xml
+UNO_JAVA_JFW_USER_DATA=${URE_OVERRIDE_JAVA_JFW_USER_DATA} 
${SYSUSERCONFIG}/URE/javasettings_${_OS}_${_ARCH}.xml
+# Third-party jars added to the JVM classpath (bsh/hsqldb/saxon when built) —
+# NOT the office API types, which travel via URE_MORE_JAVA_TYPES instead.
+UNO_JAVA_JFW_CLASSPATH_URLS=${URE_MORE_JAVA_CLASSPATH_URLS}
diff --git a/main/staging/uno.ini b/main/staging/uno.ini
index cf0d67052d..06157bbf22 100644
--- a/main/staging/uno.ini
+++ b/main/staging/uno.ini
@@ -14,6 +14,13 @@ UNO_SHARED_PACKAGES=$OOO_BASE_DIR/share/uno_packages
 UNO_SHARED_PACKAGES_CACHE=$UNO_SHARED_PACKAGES/cache
 
UNO_USER_PACKAGES=${$OOO_BASE_DIR/program/bootstrap.ini:UserInstallation}/user/uno_packages
 UNO_USER_PACKAGES_CACHE=$UNO_USER_PACKAGES/cache
-URE_INTERNAL_JAVA_CLASSPATH=${URE_MORE_JAVA_CLASSPATH_URLS}
+# Upstream (scp2 profileitem_ooo.scp) chains this to URE_MORE_JAVA_TYPES — the
+# office API/extension jars — not to URE_MORE_JAVA_CLASSPATH_URLS, which is the
+# separate bsh/hsqldb/saxon list.  Cross-referenced into fundamental.ini the 
same
+# way fundamental.ini cross-references this file, since a bare ${...} here 
would
+# resolve only against uno.ini itself.  Note the value that actually reaches
+# javavm.cxx is fundamental.ini's (theMacroExpander uses URE_BOOTSTRAP); this
+# line exists so the two files do not disagree.
+URE_INTERNAL_JAVA_CLASSPATH=${${ORIGIN}/fundamental.ini:URE_MORE_JAVA_TYPES}
 URE_INTERNAL_JAVA_DIR=${ORIGIN}/classes
 URE_INTERNAL_LIB_DIR=${ORIGIN}
diff --git a/main/stoc/readme.md b/main/stoc/readme.md
index ff6dfce734..2a44881410 100644
--- a/main/stoc/readme.md
+++ b/main/stoc/readme.md
@@ -80,69 +80,115 @@ that is a distinct type from `jchar` (`unsigned short`) — 
so every such call i
 `wchar_t == unsigned short == jchar`. `javaloader` escapes this because it only
 ever hands JNI plain UTF-8 (`NewStringUTF`).
 
-### NOT yet sufficient to run a Java component
-
-Having the DLLs is necessary but not enough. The remaining chain, in the order
-`javavm.cxx::getJavaVM` walks it:
-
-1. **`URE_INTERNAL_JAVA_DIR` must be expandable** by
-   `com.sun.star.util.theMacroExpander`, which resolves against the
-   `URE_BOOTSTRAP` file — i.e. `program/fundamental.ini`, **not** `uno.ini`.
-   `main/staging/uno.ini` already sets 
`URE_INTERNAL_JAVA_DIR=${ORIGIN}/classes`
-   and `URE_INTERNAL_JAVA_CLASSPATH=${URE_MORE_JAVA_CLASSPATH_URLS}`, but
-   `fundamental.ini` does not — and that is the one that counts. Same trap the
-   extension-path macros already hit; see the long comment in
-   `main/staging/fundamental.ini`. `URE_INTERNAL_JAVA_CLASSPATH` is allowed to
-   be unexpandable, `URE_INTERNAL_JAVA_DIR` is not.
-2. **`program/classes/` staging does not exist.** `getJavaVM` bootstraps from
-   `$URE_INTERNAL_JAVA_DIR/unoloader.jar` → `URLClassLoader` →
-   `com.sun.star.lib.unoloader.UnoClassLoader`, which then builds the real
-   classpath. Every jar target already exists, but this is **not** just a new
-   `classes/` destination in `stage_install.bzl` — three things bite:
-
-   **(a) Bazel jar names are not runtime jar names.** `java_library(name = 
"x")`
-   emits `libx.jar`, and the runtime looks up bare `unoloader.jar` etc. by 
exact
-   name (`javavm.cxx` passes the literal string). So every entry needs an
-   explicit destination name — `stage_install`'s manifest already supports a
-   third `<destname>` column, as the `.rdb` entries use:
-
-   | target | Bazel output | must stage as |
-   | --- | --- | --- |
-   | `//main/ridljar:unoloader` | `libunoloader.jar` | `unoloader.jar` |
-   | `//main/ridljar:ridl` | `libridl.jar` | `ridl.jar` — **but see (b)** |
-   | `//main/jurt:jurt` | `libjurt.jar` | `jurt.jar` |
-   | `//main/javaunohelper:juh_jar` | `libjuh_jar.jar` | `juh.jar` |
-   | `//main/unoil:offapi_java_jar` | `offapi_java_jar.jar` | `unoil.jar` |
-   | `//main/bridges:java_uno_jar` | `libjava_uno_jar.jar` | `java_uno.jar` |
-
-   **(b) `ridl.jar` has to be MERGED, not copied.** Upstream's `ridl.jar`
-   contains the generated UDK types *and* the 27 hand-written sources; here
-   those are two targets, and `java_library(name = "ridl", exports =
-   [":udkapi_java"])` exports the generated types only at COMPILE time — the
-   jar itself holds just the hand-written classes. Staging `libridl.jar`
-   as-is would ship a `ridl.jar` with no `com.sun.star.uno.*` in it. Fix is the
-   same `java_binary(create_executable = False)` deploy-jar trick used for
-   `//main/cppuhelper:cppuhelper_qa_propertysetmixin`.
-
-   **(c) The jars need MANIFESTS, which nothing currently produces.** Upstream
-   keeps them as checked-in files and `jar cfm`s them in:
-   - `main/ridljar/java/unoloader/.../manifest` — `Sealed: true`
-   - `main/jurt/util/manifest` — `Sealed: true`, un-sealing
-     `com/sun/star/lib/util/` and `com/sun/star/uno/`
-   - `main/javaunohelper/util/manifest` — `Sealed: true`,
-     `RegistrationClassName: com.sun.star.comp.JavaUNOHelperServices`,
-     `UNO-Type-Path:` with a deliberately empty value
-
-   `UnoClassLoader.getClassLoader()` reads `UNO-Type-Path` off the jar's main
-   attributes, so these are load-bearing, not cosmetic. Neither
-   `jar_from_directory` nor `java_library` currently sets a manifest —
-   `jar_from_directory` runs a bare `jar cf`. **Open question for the next
-   session:** exactly how `jurt.jar` / `ridl.jar` / `unoil.jar` reach the class
-   loader, given `URE_INTERNAL_JAVA_CLASSPATH` expands empty and juh's
-   `UNO-Type-Path` is empty — read `UnoClassLoader.getClassLoader()` and
-   `javavm.cxx`'s `UnoClassLoader` construction before wiring anything.
-3. **`jvmfwk` needs a JRE to find.** `javavendors.xml` plus the `jvmfwk3`
-   config decide which JVM is selected; without them `jvmfwk` reports no JRE
-   and `javavm` throws. Not yet staged.
-
-So the next Java-bucket step is **staging, not compilation**.
+### The runtime chain — now wired
+
+Having the DLLs is necessary but not enough. The rest of the chain, in the 
order
+`javavm.cxx::getJavaVM` walks it, is now in place:
+
+#### 1. Bootstrap variables — `program/fundamental.ini`
+
+`URE_INTERNAL_JAVA_DIR` must be expandable by
+`com.sun.star.util.theMacroExpander`, which resolves against the 
`URE_BOOTSTRAP`
+file — i.e. `program/fundamental.ini`, **not** `uno.ini`. `uno.ini` had the
+variables and `fundamental.ini` did not, which is the one that counts (same 
trap
+the extension-path macros already hit). Added there, with
+`URE_MORE_JAVA_TYPES` and the two `URE_OVERRIDE_JAVA_JFW_*_DATA` paths.
+`URE_INTERNAL_JAVA_CLASSPATH` is explicitly allowed to be unexpandable;
+`URE_INTERNAL_JAVA_DIR` is not.
+
+#### 2. `program/classes/` — how the jars actually reach the class loader
+
+This was the open question. The answer is that there are **two** channels, and
+conflating them is what made `URE_INTERNAL_JAVA_CLASSPATH` look optional:
+
+- **Hardcoded names.** `javavm.cxx` builds a plain `URLClassLoader` over the
+  single literal `"$URE_INTERNAL_JAVA_DIR/unoloader.jar"`, loads
+  `com.sun.star.lib.unoloader.UnoClassLoader` from it, and instantiates that 
with
+  `base` = the same directory. `UnoClassLoader.createUrls()` then appends four
+  more literals — `java_uno.jar`, `juh.jar`, `jurt.jar`, `ridl.jar`. **No
+  manifest, no classpath variable and no `UNO-Type-Path` is involved** for 
these
+  five; the filename *is* the lookup key.
+- **The classPath argument.** `unoil.jar` is *not* one of the five. It reaches
+  the loader only as the `classPath` constructor argument, which is
+  `$URE_INTERNAL_JAVA_CLASSPATH` → `URE_MORE_JAVA_TYPES` (upstream:
+  `$ORIGIN/classes/unoil.jar $ORIGIN/classes/ScriptFramework.jar` + each
+  extension's `UNO_JAVA_CLASSPATH`, from `scp2` `profileitem_ooo.scp`). So an
+  empty `URE_INTERNAL_JAVA_CLASSPATH` does not merely lose extensions — it 
loses
+  every `com.sun.star.*` office API type.
+
+`UNO-Type-Path` matters at a different point entirely:
+`UnoClassLoader.getClassLoader()` reads it when *javaloader* opens a 
**component**
+jar, to decide which extra type jars to hoist into the root loader. juh's
+deliberately empty value suppresses that; an absent one falls back to `"<>"`,
+which re-adds the jar itself — already present via `createUrls()`, so a 
harmless
+duplicate. That is why the juh manifest is reproduced only for
+`RegistrationClassName` (load-bearing: `juh.jar` is a registered component in
+`services.rdb`) and not for the rest.
+
+The three build-side problems are solved by one new rule,
+`uno_jar` in `build/rules/java_pipeline.bzl` (singlejar):
+
+| target | produces | note |
+| --- | --- | --- |
+| `//main/ridljar:unoloader_jar` | `unoloader.jar` | |
+| `//main/ridljar:ridl_jar` | `ridl.jar` | **merge** of `:ridl` + 
`:udkapi_java_jar` |
+| `//main/jurt:jurt_jar` | `jurt.jar` | |
+| `//main/javaunohelper:juh_runtime_jar` | `juh.jar` | + 
`RegistrationClassName` |
+| `//main/bridges:java_uno_runtime_jar` | `java_uno.jar` | |
+| `//main/unoil:unoil_jar` | `unoil.jar` | reaches the loader via the 
classPath channel |
+
+- **Exact names**: `out =` names the output at the producing target, so the 
name
+  lives next to the code that justifies it instead of in a staging rename 
table.
+- **`ridl.jar` is a MERGE, not a rename.** Upstream's single jar holds the
+  javamaker-generated UDK types *and* the 27 hand-written sources; here they 
are
+  two targets and `java_library(exports = [":udkapi_java"])` wires them 
together
+  only at COMPILE time, so `libridl.jar` alone has no `com.sun.star.uno.*`.
+  (Verified: the merged jar has 436 classes including `XInterface`, `TypeClass`
+  and `UnoRuntime`.)
+- **Manifests**: neither `java_library` nor `jar_from_directory` (a bare
+  `jar cf`) writes one.
+
+Staged flat into `program/classes/` by `//main/staging:_install_classes`, 
using a
+new `flatten` option on `tree_install` (the six jars come from five packages, 
so
+no single `strip_prefix` applies).
+
+**DIVERGENCE — `Sealed:` is omitted.** Upstream seals these jars, with jurt's
+manifest un-sealing `com/sun/star/lib/util/` and `com/sun/star/uno/` because
+those packages are split across `jurt.jar` and `ridl.jar`. A per-package 
manifest
+section cannot be expressed through singlejar's `--deploy_manifest_lines`, 
which
+writes the main section only; a blanket `Sealed: true` would therefore be
+*stricter* than upstream and throw `SecurityException` on exactly that split.
+Sealing only ever restricts and never enables, so omitting it is the safe
+direction. Reversible if the jars ever need integrity sealing.
+
+#### 3. `jvmfwk` needs a JRE to find — and had two of its own gaps
+
+- **`jvmfwk.dll` was built without `SOLAR_JAVA`**, which is not a feature 
switch
+  here either: `framework.cxx` compiles `jfw_startVM()` down to a bare
+  `return JFW_E_ERROR;` under `#ifndef SOLAR_JAVA`. Every caller would have 
got a
+  generic error no matter how correct the configuration was. Now defined (the
+  exported ABI is unchanged — the JNI types appear only behind pointers, so
+  consumers that include `jvmfwk/framework.h` without it still link).
+- **`sunjavaplugin.dll` was never built.** `jvmfwk` itself contains no JRE
+  knowledge; `javavendors.xml` maps every supported vendor to
+  `vnd.sun.star.expand:$URE_INTERNAL_LIB_DIR/sunjavaplugin.dll`, which jvmfwk
+  `osl_loadModule()`s. It is a private plugin interface (4 exports from
+  `sunjavaplugin.map`), *not* a UNO component — no `component_getFactory`, and
+  nothing in `services.rdb`. Nothing links it, so it must be listed explicitly 
in
+  staging. See `main/jvmfwk/readme.md`.
+- **`javavendors.xml` and `jvmfwk3.ini`** are now staged into `program/`. The 
ini
+  name is not ours to choose: `jvmfwk/source/fwkutil.hxx` hardcodes
+  `SAL_CONFIGFILE("/jvmfwk3")` relative to its own library directory, so it 
stays
+  `jvmfwk3.ini` even though the library is built as `jvmfwk.dll` here.
+
+#### Still open
+
+- **No `Package_jvmfwk` equivalent for `sunjavapluginrc`.** Upstream ships it
+  next to the plugin with `JFW_PLUGIN_DO_NOT_CHECK_ACCESSIBILITY=1`; jvmfwk 
reads
+  that key through the *default* `rtl::Bootstrap`, not a plugin-private ini, so
+  the file appears to be inert in an office install. Left unstaged; the 
fallback
+  is a registry probe of 
`HKCU\Software\OpenOffice\Accessibility\AtToolSupport`.
+- **End-to-end verification.** The chain builds and stages; it has not yet been
+  exercised by loading an actual Java UNO component. The natural first probe is
+  `cppuhelper/qa/propertysetmixin`'s three red `testJava*` cases, which need
+  exactly this (plus that suite's own Java component jar).
diff --git a/main/unoil/BUILD.bazel b/main/unoil/BUILD.bazel
index ec128c42fe..40b69a0f50 100644
--- a/main/unoil/BUILD.bazel
+++ b/main/unoil/BUILD.bazel
@@ -1,4 +1,4 @@
-load("//build/rules:java_pipeline.bzl", "jar_from_directory", 
"javamaker_classes")
+load("//build/rules:java_pipeline.bzl", "jar_from_directory", 
"javamaker_classes", "uno_jar")
 load("@rules_java//java:defs.bzl", "java_import")
 
 # ── Step 1: Generate Java .class files from offapi.rdb ───────────────────────
@@ -26,3 +26,19 @@ java_import(
     exports    = ["//main/ridljar:ridl"],
     visibility = ["//visibility:public"],
 )
+
+# ── Step 4: unoil.jar → program/classes/ ─────────────────────────────────────
+# Unlike the other five runtime jars, unoil.jar is NOT one of the names
+# UnoClassLoader.createUrls() hardcodes.  It reaches the class loader through
+# the OTHER channel: javavm.cxx expands $URE_INTERNAL_JAVA_CLASSPATH and passes
+# the result as the UnoClassLoader classPath argument.  Upstream chains that to
+# URE_MORE_JAVA_TYPES = "$ORIGIN/classes/unoil.jar $ORIGIN/classes/
+# ScriptFramework.jar ..." in fundamentalbasisrc; main/staging/fundamental.ini
+# does the same (minus ScriptFramework, not built yet).  So an empty
+# URE_INTERNAL_JAVA_CLASSPATH is precisely what loses the office API types.
+uno_jar(
+    name       = "unoil_jar",
+    out        = "unoil.jar",
+    jars       = [":offapi_java_jar"],
+    visibility = ["//visibility:public"],
+)

Reply via email to