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 b36ee9e70abf6566da3cfe6ccb96aeefdcbccc75
Author: Peter Kovacs <[email protected]>
AuthorDate: Mon Jul 27 07:07:41 2026 +0200

    build(bazel): stage the pyuno Python runtime so macros can load and run
    
    Building pyuno.dll/pythonloader.dll was not enough: anything that touches a
    Python script provider instantiates com.sun.star.loader.Python, and each
    missing piece failed differently.
    
    Bootstrap + stdlib
      pyuno_loader.cxx reads PYUNO_LOADER_PYTHONHOME/PYTHONPATH from
      program/pythonloader.uno.ini (scp2 profileitem_python.scp); that file did
      not exist, and the python-core-2.7.18 stdlib tree was wired only into
      :install_all, not :install.  Without either, Py_Initialize() finds no
      stdlib and Python 2.7 calls Py_FatalError -> exit(), killing the whole
      soffice process with no exception and no crash dump.
    
    pyuno.pyd
      Python 2.7's _PyImport_DynLoadFiletab (Python/dynload_win.c) accepts only
      ".pyd" on Windows, never ".dll", and uno.py opens with "import pyuno".
      Bazel always names a linkshared cc_binary "<target>.dll", so 
util/pyuno.def
      carries a lone "LIBRARY pyuno.pyd" -- setting the DLL's internal name, and
      hence the name in pyuno.if.lib and in every consumer's import table -- and
      the new //build/rules:copy_file re-exposes the file as pyuno.pyd.  
Verified
      with VC9 link.exe: export directory, implib members and a consumer's 
import
      table all read pyuno.pyd.  Expect a harmless LNK4070; /OUT still owns the
      filename on disk.  Only pyuno.pyd is loaded -- pyuno.cxx and
      pyuno_runtime.cxx have file-level PyTypeObject statics, so two HMODULEs
      would mean two type objects rejecting each other's instances.
    
    Install allowlist
      _flat_install_impl filters program/ by extension and had no "pyd", so
      pyuno.pyd was collected (visible in cquery) and then silently dropped.
    
    Python-side modules
      pythonloader.py / uno.py / unohelper.py and the pyprov provider
      (pythonscript.py, officehelper.py, mailmerge.py) now stage flat into
      program/, the last entry of PYUNO_LOADER_PYTHONPATH; the example scripts
      go to share/Scripts/python so the "OpenOffice Macros" container resolves.
    
    Upstream does all of this from scp2 + Package_*.mk, so there is nothing to
    backport to dmake.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
---
 build/rules/copy_file.bzl             | 31 ++++++++++++++
 main/pyuno/BUILD.bazel                | 44 ++++++++++++++++++--
 main/pyuno/readme.md                  | 77 +++++++++++++++++++++++++++++++++++
 main/pyuno/util/pyuno.def             | 27 ++++++++++++
 main/scripting/BUILD.bazel            | 35 ++++++++++++++++
 main/staging/BUILD.bazel              | 38 +++++++++++++----
 main/staging/collect_files_aspect.bzl |  5 ++-
 main/staging/pythonloader.uno.ini     | 12 ++++++
 8 files changed, 257 insertions(+), 12 deletions(-)

diff --git a/build/rules/copy_file.bzl b/build/rules/copy_file.bzl
new file mode 100644
index 0000000000..4a572b9def
--- /dev/null
+++ b/build/rules/copy_file.bzl
@@ -0,0 +1,31 @@
+"""copy_file — re-expose a single file under a different name.
+
+Pure Bazel (ctx.actions.symlink), no shell: cmd.exe `copy` chokes on the
+forward-slash paths Bazel hands to genrules, and the staging rules already
+symlink their inputs, so an extra link level costs nothing.
+
+Used to give a cc_binary output a name Bazel cannot produce itself: a
+linkshared cc_binary is always named "<target>.dll" on Windows, but Python
+2.7's extension-module loader only accepts ".pyd" (Python/dynload_win.c).
+"""
+
+def _copy_file_impl(ctx):
+    out = ctx.actions.declare_file(ctx.attr.out)
+    ctx.actions.symlink(output = out, target_file = ctx.file.src)
+    return [DefaultInfo(files = depset([out]))]
+
+copy_file = rule(
+    implementation = _copy_file_impl,
+    doc = "Copies (symlinks) src to a file named `out` in this package.",
+    attrs = {
+        "src": attr.label(
+            allow_single_file = True,
+            mandatory = True,
+            doc = "Single file to re-expose.",
+        ),
+        "out": attr.string(
+            mandatory = True,
+            doc = "Basename of the produced file.",
+        ),
+    },
+)
diff --git a/main/pyuno/BUILD.bazel b/main/pyuno/BUILD.bazel
index 8fa9924547..2dfcc28a28 100644
--- a/main/pyuno/BUILD.bazel
+++ b/main/pyuno/BUILD.bazel
@@ -1,6 +1,7 @@
 package(default_visibility = ["//visibility:public"])
 
 load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library")
+load("//build/rules:copy_file.bzl", "copy_file")
 
 _DEFINES = [
     "WNT", "GUI", "WIN32",
@@ -64,10 +65,21 @@ cc_library(
     visibility = ["//visibility:public"],
 )
 
-# ── pyuno.dll ─────────────────────────────────────────────────────
-# Core Python-UNO bridge DLL.  On Windows Python extension modules
-# conventionally use the .pyd extension; Bazel produces .dll — rename
-# at install time if needed.
+# ── pyuno.pyd ─────────────────────────────────────────────────────
+# Core Python-UNO bridge.  Windows Python extension modules MUST use the .pyd
+# extension — Python 2.7's _PyImport_DynLoadFiletab (Python/dynload_win.c) 
lists
+# only {".pyd"} ({"_d.pyd"} under _DEBUG), NOT ".dll".  uno.py starts with
+# `import pyuno`, so with a pyuno.dll that import raises ImportError, `import
+# pythonloader` fails with it, and ScriptProviderForPython can never be
+# instantiated (macro dialog shows the Python node from pyuno.xcd but lists
+# nothing and offers no Create).  Upstream: DLLPOST=.pyd in
+# source/module/makefile.mk, scp2 ships "pyuno.pyd".
+#
+# Bazel names a linkshared cc_binary "<target>.dll" unconditionally, so the 
file
+# is produced as pyuno.dll and re-exposed as pyuno.pyd by :pyuno_pyd below;
+# util/pyuno.def carries `LIBRARY pyuno.pyd` so consumers' import tables name
+# the .pyd.  Only ONE of the two may be staged — see util/pyuno.def for why.
+#
 # Exports: initpyuno (Python 2.x), Runtime, PyRef, PyThreadAttach,
 #          PyThreadDetach via PY_DLLEXPORT = SAL_DLLPUBLIC_EXPORT.
 # pyuno_dlopenwrapper.c is *nix-only (uses dlopen); omitted on Windows.
@@ -91,6 +103,15 @@ cc_binary(
     additional_linker_inputs = _LINK_DEPS,
     linkshared = True,
     linkopts = _LINKOPTS,
+    win_def_file = "util/pyuno.def",
+    visibility = ["//visibility:public"],
+)
+
+# The staged artifact: same file, the name Python's importer will accept.
+copy_file(
+    name = "pyuno_pyd",
+    src  = ":pyuno",
+    out  = "pyuno.pyd",
     visibility = ["//visibility:public"],
 )
 
@@ -119,5 +140,20 @@ cc_binary(
     visibility = ["//visibility:public"],
 )
 
+# ── Python-side runtime modules → staged flat into program/ 
───────────────────
+# pyuno_loader.cxx imports the "pythonloader" module, which imports "uno";
+# unohelper is pulled in by most Python components.  PYUNO_LOADER_PYTHONPATH
+# ends with $ORIGIN (= program/), so all three must sit there as plain files.
+# Equivalent of Package_py.mk / scp2 file_python.scp (Dir = program).
+filegroup(
+    name = "pyuno_py",
+    srcs = [
+        "source/loader/pythonloader.py",
+        "source/module/uno.py",
+        "source/module/unohelper.py",
+    ],
+    visibility = ["//visibility:public"],
+)
+
 exports_files(glob(["**/*.component"]))
 
diff --git a/main/pyuno/readme.md b/main/pyuno/readme.md
index 695762bdd8..26b44cf141 100644
--- a/main/pyuno/readme.md
+++ b/main/pyuno/readme.md
@@ -7,6 +7,8 @@ Builds the Python-UNO bridge from `main/pyuno/`.
 | Target | Output |
 |--------|--------|
 | `//main/pyuno:pyuno` | `pyuno.dll` — core bridge (also `pyuno_implib` for 
linking) |
+| `//main/pyuno:pyuno_pyd` | `pyuno.pyd` — the same file under the name Python 
imports; **this** is what gets staged |
+| `//main/pyuno:pyuno_py` | `pythonloader.py` / `uno.py` / `unohelper.py` → 
`program/` |
 | `//main/pyuno:pythonloader` | `pythonloader.dll` — UNO component loader |
 | `//main/pyuno:pyuno_headers` | `inc/pyuno/pyuno.hxx` — public C++ bridge 
headers |
 | `//main/pyuno:pyuno_implib` | `pyuno.if.lib` — import library for downstream 
consumers |
@@ -25,8 +27,83 @@ Built from `source/loader/pyuno_loader.cxx`.
 UNO component; exports `component_getImplementationEnvironment` and
 `component_getFactory` via `SAL_DLLPUBLIC_EXPORT` — no DEF file needed.
 
+## Runtime requirements (staging) — three separate landmines
+
+Building the DLLs is not enough; `com.sun.star.loader.Python` is instantiated 
by
+anything that touches a Python script provider (Tools ▸ Macros, script-provider
+enumeration), and each missing piece fails differently:
+
+1. **`program/pythonloader.uno.ini`** — read by `pyuno_loader.cxx` 
`CreateInstance()`
+   for `PYUNO_LOADER_PYTHONHOME` / `PYUNO_LOADER_PYTHONPATH` (scp2
+   `profileitem_python.scp`).  Missing ⇒ `Py_Initialize()` finds no stdlib ⇒
+   `Py_FatalError` ⇒ **`exit()` kills the whole soffice process**, no 
exception,
+   no crash dump — just `NtTerminateProcess` right after `pyuno` loads.
+   Source: `main/staging/pythonloader.uno.ini`.
+2. **`program/python-core-2.7.18/lib`** — the stdlib itself, same failure mode.
+   Staged by `//main/staging:install_python_lib`, part of `:install`.
+3. **`pyuno.pyd` naming** — see below.
+
+The `.py` files (`pythonloader.py`, `uno.py`, `unohelper.py` here;
+`pythonscript.py`, `officehelper.py`, `mailmerge.py` from
+`//main/scripting:pyprov_py`) must sit flat in `program/`, the last entry of
+`PYUNO_LOADER_PYTHONPATH` (`$ORIGIN`).
+
 ## Key build decisions
 
+### `.pyd`, not `.dll` — and exactly one copy
+Python 2.7's Windows extension loader accepts only one suffix:
+
+```c
+/* Python/dynload_win.c */
+const struct filedescr _PyImport_DynLoadFiletab[] = {
+#ifdef _DEBUG
+    {"_d.pyd", "rb", C_EXTENSION},
+#else
+    {".pyd", "rb", C_EXTENSION},
+#endif
+    {0, 0}
+};
+```
+
+`uno.py` opens with `import pyuno`, so a `pyuno.dll` yields
+`ImportError: No module named pyuno`, `import pythonloader` fails with it, and
+`ScriptProviderForPython` can never be instantiated.  The symptom is quiet: the
+macro organizer still shows the **Python** language node (that comes from the
+staged `pyuno.xcd`), but the tree is empty and *Create* stays greyed out —
+`SvxScriptOrgDialog::CheckButtons` enables it from the browse node's
+`Creatable` property, and there is no node to ask.
+
+Upstream builds one file called `pyuno.pyd` (`source/module/makefile.mk`
+`DLLPOST=.pyd`), which is also what `pythonloader` links against.  Bazel cannot
+do that directly — a `linkshared` `cc_binary` is always `<target>.dll` — so:
+
+* `util/pyuno.def` carries a single `LIBRARY pyuno.pyd` statement, which sets
+  the DLL's internal name and hence the name recorded in `pyuno.if.lib` and in
+  `pythonloader.dll`'s import table.  `/OUT` still wins for the file on disk.
+* `copy_file` (`//build/rules:copy_file.bzl`) re-exposes that file as
+  `pyuno.pyd`, which is what staging depends on.
+
+`pyuno.dll` still lands in `program/` too, as a by-product: the staging aspect
+walks `additional_linker_inputs`, so `pythonloader`'s `:pyuno_implib`
+(`filegroup(srcs = [":pyuno"], output_group = "interface_library")`) drags the
+`cc_binary`'s own output along.  Inert — with the `LIBRARY` override nothing
+names `pyuno.dll` any more, so it is never loaded; `pyuno.pyd` is the module.
+
+VC9 warns `LNK4070: /OUT:pyuno.pyd directive in .EXP differs from output
+filename 'pyuno.dll'; ignoring directive` — expected, and it ignores the
+directive only for the output filename.  Verified against link.exe
+9.00.30729.01: export directory, import-library members, and a consumer's
+import table all read `pyuno.pyd`.  Check after a rebuild with
+`link /dump /imports bazel-bin/main/pyuno/pythonloader.dll`.
+
+Staging both names would be wrong, not merely redundant: `pyuno.cxx` has a
+file-level `static PyTypeObject PyUNOType` and `pyuno_runtime.cxx` a
+`static PyTypeObject RuntimeImpl_Type`.  Two HMODULEs ⇒ two distinct type
+objects ⇒ each half rejects the other's objects (`getRuntimeImpl` looks up
+`pyuno_runtime` in the builtins dict and type-checks it).  Upstream gets one
+instance for free because `pythonloader`'s import and Python's `import pyuno`
+resolve to the same path.
+
 ### pyconfig.h overlay — no auto-link pragma
 Python 2.7's `PC/pyconfig.h` contains:
 ```c
diff --git a/main/pyuno/util/pyuno.def b/main/pyuno/util/pyuno.def
new file mode 100644
index 0000000000..3d196fb70c
--- /dev/null
+++ b/main/pyuno/util/pyuno.def
@@ -0,0 +1,27 @@
+; Windows module-definition file for the Python-UNO bridge.
+;
+; No EXPORTS section: every exported symbol already carries
+; SAL_DLLPUBLIC_EXPORT / PY_DLLEXPORT (__declspec(dllexport)).
+;
+; The single LIBRARY statement is the whole point of this file.  It sets the
+; DLL's *internal* name, which is what the linker records in the generated
+; import library (pyuno.if.lib) and therefore in the import table of every
+; consumer -- currently pythonloader.dll.  Bazel's /OUT still names the file on
+; disk pyuno.dll; the copy_file rule in BUILD.bazel re-exposes it as pyuno.pyd,
+; the name Python 2.7 requires (Python/dynload_win.c accepts ONLY ".pyd").
+;
+; Both halves must agree, because pyuno keeps per-DLL statics -- PyUNOType
+; (pyuno.cxx) and RuntimeImpl_Type (pyuno_runtime.cxx).  Shipping the module
+; twice, once as pyuno.dll for pythonloader's import table and once as
+; pyuno.pyd for `import pyuno`, would give two HMODULEs with two distinct
+; PyTypeObjects, and each half would reject the other's objects.  Upstream has
+; exactly one file (pyuno/source/module/makefile.mk: DLLPOST=.pyd); so do we.
+;
+; EXPECTED, harmless: VC9 emits
+;   pyuno.if.exp : warning LNK4070: /OUT:pyuno.pyd directive in .EXP differs
+;                  from output filename 'pyuno.dll'; ignoring directive
+; It ignores the directive only for the *output filename* (which is what we
+; want -- Bazel owns that).  Verified with VC9 link.exe 9.00.30729.01: the
+; export directory, the import-library members, and a consumer's import table
+; all name pyuno.pyd.
+LIBRARY pyuno.pyd
diff --git a/main/scripting/BUILD.bazel b/main/scripting/BUILD.bazel
index 8ceb3e20fc..cd46537e49 100644
--- a/main/scripting/BUILD.bazel
+++ b/main/scripting/BUILD.bazel
@@ -305,5 +305,40 @@ rsc_res(
     visibility = ["//visibility:public"],
 )
 
+# ── Python script provider (pyprov) → staged flat into program/ 
───────────────
+# pythonscript.component is registered in services.rdb with the URI
+# vnd.openoffice.pymodule:pythonscript, so pythonloader resolves the
+# implementation by doing `import pythonscript` off PYTHONPATH, whose last
+# entry is $ORIGIN (= program/).  Without these files the macro dialog still
+# shows the Python language node (that comes from pyuno.xcd config) but the
+# provider cannot be instantiated — no scripts listed, none can be added.
+# officehelper is imported by the examples; mailmerge is the mail-merge
+# component, registered the same way.  Equivalent of source/pyprov/makefile.mk
+# copying them to $(DLLDEST) and scp2 file_python.scp (Dir = program).
+filegroup(
+    name = "pyprov_py",
+    srcs = [
+        "source/pyprov/pythonscript.py",
+        "source/pyprov/officehelper.py",
+        "source/pyprov/mailmerge.py",
+    ],
+    visibility = ["//visibility:public"],
+)
+
+# ── Example Python scripts → share/Scripts/python/ ("OpenOffice Macros") ─────
+# MyUriHelper in pythonscript.py maps the "share" location to
+# <BaseInstallation>/share/Scripts/python; the folder has to exist for that
+# container to enumerate.  Upstream ships it as ScriptsPython.zip (ARCHIVE
+# style, extracted on install — examples/makefile.mk ZIP4LIST); Bazel stages
+# the tree directly.
+filegroup(
+    name = "python_example_scripts",
+    srcs = [
+        "examples/python/HelloWorld.py",
+        "examples/python/Capitalise.py",
+    ] + glob(["examples/python/pythonSamples/**"]),
+    visibility = ["//visibility:public"],
+)
+
 exports_files(glob(["**/*.component"]))
 
diff --git a/main/staging/BUILD.bazel b/main/staging/BUILD.bazel
index 615c7956df..79f05f2099 100644
--- a/main/staging/BUILD.bazel
+++ b/main/staging/BUILD.bazel
@@ -9,6 +9,8 @@ filegroup(
         "fundamental.ini",
         "soffice.ini",
         "uno.ini",
+        # PYUNO_LOADER_PYTHONHOME / PYTHONPATH for pythonloader.dll.
+        "pythonloader.uno.ini",
     ],
 )
 
@@ -148,8 +150,16 @@ collect_outputs(
         "//main/scripting:vbaevents.uno",
         "//main/scripting:stringresource.uno",
         "//main/scripting:protocolhandler",
-        "//main/pyuno:pyuno",
+        # pyuno.pyd, NOT pyuno.dll: Python 2.7 imports only ".pyd".  Staging
+        # both would load the bridge twice under two HMODULEs, each with its 
own
+        # PyUNOType/RuntimeImpl_Type statics — see main/pyuno/util/pyuno.def.
+        "//main/pyuno:pyuno_pyd",
         "//main/pyuno:pythonloader",
+        # pythonloader.py / uno.py / unohelper.py — imported by the loader.
+        "//main/pyuno:pyuno_py",
+        # pythonscript.py — the ScriptProviderForPython implementation itself,
+        # resolved by `import pythonscript` from the services.rdb pymodule URI.
+        "//main/scripting:pyprov_py",
 
         # ── VBA 
───────────────────────────────────────────────────────────────
         "//main/vbahelper:vbahelper",
@@ -490,6 +500,14 @@ tree_install(
     dst          = "share/autotext",
 )
 
+# share/Scripts/python — the "OpenOffice Macros" Python container.
+tree_install(
+    name = "_install_python_scripts",
+    src          = "//main/scripting:python_example_scripts",
+    strip_prefix = "main/scripting/examples/python",
+    dst          = "share/Scripts/python",
+)
+
 tree_install(
     name = "_install_wordbook",
     src          = "//main/extras:wordbook_files",
@@ -630,6 +648,13 @@ filegroup(
         ":_install_images_zip",
         ":_install_images_industrial_zip",
         ":_install_uiconfig",
+        ":_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
+        # its stdlib.  Anything that instantiates com.sun.star.loader.Python
+        # (Tools > Macros, script-provider enumeration) would kill soffice.
+        ":install_python_lib",
+        ":install_python_bin",
     ],
     visibility = ["//visibility:public"],
 )
@@ -652,12 +677,11 @@ tree_install(
     dst          = "program/python-core-2.7.18/bin",
 )
 
-# ── install_all: build everything in one shot 
─────────────────────────────────
+# ── install_all: kept as an alias for :install 
────────────────────────────────
+# The python-core tree used to live only here; it is now part of :install 
itself
+# (see above), so the two are identical.  Retained so existing invocations and
+# docs referring to //main/staging:install_all keep working.
 filegroup(
     name = "install_all",
-    srcs = [
-        ":install",
-        ":install_python_lib",
-        ":install_python_bin",
-    ],
+    srcs = [":install"],
 )
diff --git a/main/staging/collect_files_aspect.bzl 
b/main/staging/collect_files_aspect.bzl
index 031dbc93f1..0164b5bb9a 100644
--- a/main/staging/collect_files_aspect.bzl
+++ b/main/staging/collect_files_aspect.bzl
@@ -109,7 +109,10 @@ res_stage = rule(
 )
 
 # Extensions included in the flat install directory.
-_INSTALL_EXTS = {"exe": True, "dll": True, "rdb": True, "zip": True, "py": 
True, "ini": True, "manifest": True}
+# "pyd": Python extension modules.  Python 2.7's importer accepts ONLY this
+# suffix on Windows (Python/dynload_win.c), so pyuno ships as pyuno.pyd; 
without
+# it here the file is collected but silently dropped on the way to program/.
+_INSTALL_EXTS = {"exe": True, "dll": True, "pyd": True, "rdb": True, "zip": 
True, "py": True, "ini": True, "manifest": True}
 
 def _flat_install_impl(ctx):
     all_files = depset(transitive = [
diff --git a/main/staging/pythonloader.uno.ini 
b/main/staging/pythonloader.uno.ini
new file mode 100644
index 0000000000..4ba1b2141a
--- /dev/null
+++ b/main/staging/pythonloader.uno.ini
@@ -0,0 +1,12 @@
+[Bootstrap]
+# Read by pyuno/source/loader/pyuno_loader.cxx CreateInstance() from
+# $OOO_BASE_DIR/program/pythonloader.uno.ini 
(SAL_CONFIGFILE("pythonloader.uno")).
+# Both keys MUST be set before Py_Initialize(); without them Python 2.7 finds 
no
+# stdlib (no os.py/site.py), calls Py_FatalError and exit()s the whole soffice
+# process the instant com.sun.star.loader.Python is instantiated.
+#
+# Equivalent of scp2/source/python/profileitem_python.scp (WNT, non-gcc 
branch).
+# PYTHONPATH entries are space-separated file URLs; the loader converts each 
to a
+# system path and joins them with ';'.  No lib-dynload / lib-tk on Windows.
+PYUNO_LOADER_PYTHONHOME=$ORIGIN/python-core-2.7.18
+PYUNO_LOADER_PYTHONPATH=$ORIGIN/python-core-2.7.18/lib 
$ORIGIN/python-core-2.7.18/lib/site-packages $ORIGIN

Reply via email to