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
