wineole 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wineole-0.1.0/LICENSE +21 -0
- wineole-0.1.0/PKG-INFO +264 -0
- wineole-0.1.0/README.md +248 -0
- wineole-0.1.0/pyproject.toml +25 -0
- wineole-0.1.0/setup.cfg +4 -0
- wineole-0.1.0/tests/test_client.py +309 -0
- wineole-0.1.0/tests/test_client_open.py +103 -0
- wineole-0.1.0/tests/test_errors.py +24 -0
- wineole-0.1.0/tests/test_integration.py +205 -0
- wineole-0.1.0/tests/test_proxy.py +224 -0
- wineole-0.1.0/tests/test_wineole_module.py +98 -0
- wineole-0.1.0/wineole/__init__.py +82 -0
- wineole-0.1.0/wineole/client.py +208 -0
- wineole-0.1.0/wineole/dist/aarch64-pc-windows-gnullvm/wineole-bridge.exe +0 -0
- wineole-0.1.0/wineole/dist/i686-pc-windows-gnu/wineole-bridge.exe +0 -0
- wineole-0.1.0/wineole/dist/x86_64-pc-windows-gnu/wineole-bridge.exe +0 -0
- wineole-0.1.0/wineole/errors.py +20 -0
- wineole-0.1.0/wineole/proxy.py +169 -0
- wineole-0.1.0/wineole.egg-info/PKG-INFO +264 -0
- wineole-0.1.0/wineole.egg-info/SOURCES.txt +20 -0
- wineole-0.1.0/wineole.egg-info/dependency_links.txt +1 -0
- wineole-0.1.0/wineole.egg-info/top_level.txt +1 -0
wineole-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 firelzrd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
wineole-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wineole
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Drive Win32OLE/COM automation of Wine-hosted Windows apps from Python
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/firelzrd/wineole
|
|
7
|
+
Project-URL: Repository, https://github.com/firelzrd/wineole
|
|
8
|
+
Project-URL: Issues, https://github.com/firelzrd/wineole/issues
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# WineOLE
|
|
18
|
+
|
|
19
|
+
Drive Windows OLE/COM automation (Excel, Word, ...) from Linux, through Wine.
|
|
20
|
+
A small Windows-side bridge process (`wineole-bridge`) runs under `wine` and
|
|
21
|
+
speaks a JSON Lines RPC protocol to native Python and Ruby clients, so host
|
|
22
|
+
code can script a Windows application as if it were a local object.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
Python:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
pip install wineole
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Ruby:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
gem install wineole
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Both packages bundle the prebuilt `wineole-bridge` binaries -- no separate
|
|
39
|
+
build step, and no Windows build of Python or Ruby is needed.
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
The quickest way to get started -- open the default bridge and create a
|
|
44
|
+
COM object in one call each:
|
|
45
|
+
|
|
46
|
+
Python:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import wineole
|
|
50
|
+
|
|
51
|
+
xl = wineole.create('Excel.Application')
|
|
52
|
+
xl.Visible = True
|
|
53
|
+
xl.Workbooks().Add()
|
|
54
|
+
xl.Worksheets()[1].Range('A1').Value = 'Hello from Python'
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Ruby:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
require 'wineole'
|
|
61
|
+
|
|
62
|
+
xl = WineOLE.create('Excel.Application')
|
|
63
|
+
xl.Visible = true
|
|
64
|
+
xl.Workbooks.Add
|
|
65
|
+
xl.Worksheets[1].Range('A1').Value = 'Hello from Ruby'
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`WineOLE.create`/`wineole.create()` (create a new instance), `.connect`/
|
|
69
|
+
`connect()` (attach to an already-running instance, failing if none exists),
|
|
70
|
+
and `.connect_or_create`/`connect_or_create()` (attach if something's
|
|
71
|
+
running, otherwise create -- the classic VBScript `GetObject`-or-
|
|
72
|
+
`CreateObject` idiom) all lazily open a default bridge connection the first
|
|
73
|
+
time any of them is called, then reuse it -- no separate client variable
|
|
74
|
+
needed for the common case. `WineOLE.close`/`wineole.close()` releases that
|
|
75
|
+
implicit default early if you want to (otherwise it lives for the process's
|
|
76
|
+
lifetime).
|
|
77
|
+
|
|
78
|
+
For anything beyond the zero-config case -- a specific host/port, an auth
|
|
79
|
+
token, more than one bridge connection at once -- use `Client.open`
|
|
80
|
+
explicitly and create objects on it directly:
|
|
81
|
+
|
|
82
|
+
Python:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
import wineole
|
|
86
|
+
|
|
87
|
+
client = wineole.Client.open()
|
|
88
|
+
xl = client.create('Excel.Application')
|
|
89
|
+
|
|
90
|
+
xl.Visible = True
|
|
91
|
+
xl.Workbooks().Add()
|
|
92
|
+
xl.Worksheets()[1].Range('A1').Value = 'Hello from Python'
|
|
93
|
+
|
|
94
|
+
client.close()
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Ruby:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
require 'wineole'
|
|
101
|
+
|
|
102
|
+
client = WineOLE::Client.open
|
|
103
|
+
xl = client.create('Excel.Application')
|
|
104
|
+
|
|
105
|
+
xl.Visible = true
|
|
106
|
+
xl.Workbooks.Add
|
|
107
|
+
xl.Worksheets[1].Range('A1').Value = 'Hello from Ruby'
|
|
108
|
+
|
|
109
|
+
client.close
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`WineOLE.open`/`wineole.open()` sits between the two: it's `Client.open`
|
|
113
|
+
reachable without typing `Client`, and it *also* becomes the implicit
|
|
114
|
+
default, so a `WineOLE.create`/`wineole.create()` call right after uses the
|
|
115
|
+
client you just explicitly configured rather than a second, separate
|
|
116
|
+
zero-config one.
|
|
117
|
+
|
|
118
|
+
The two clients are otherwise near-identical in capability -- the extra `()`
|
|
119
|
+
in the Python version is the one real syntactic difference: Python's
|
|
120
|
+
`__getattr__` can't tell a property read from a method call apart the way
|
|
121
|
+
Ruby's `method_missing` can, so every COM member access returns a callable
|
|
122
|
+
that has to actually be called. See `bindings/ruby/examples/` and
|
|
123
|
+
`bindings/python/examples/` for a larger, visual demo of both.
|
|
124
|
+
|
|
125
|
+
### Meta-methods and the `ole_` prefix
|
|
126
|
+
|
|
127
|
+
A `Proxy` forwards any name it doesn't recognize straight to the remote COM
|
|
128
|
+
object, so its own bookkeeping methods are named with an `ole_` prefix --
|
|
129
|
+
`ole_handle`, `ole_session_id`, `ole_release`, `ole_const_load`, and
|
|
130
|
+
`ole_created?`/`ole_created` (below) -- to keep them out of the way of a
|
|
131
|
+
real COM member that happens to share the plain name. `invoke` is the one
|
|
132
|
+
deliberate exception: it's kept bare and public as an explicit escape hatch
|
|
133
|
+
for the rare case where a COM object really does define a member called
|
|
134
|
+
e.g. `ole_handle` -- `proxy.invoke('ole_handle', [], {})` reaches it
|
|
135
|
+
directly, bypassing the local method entirely. This mirrors real Ruby
|
|
136
|
+
`WIN32OLE`'s own `ole_*`-prefixed introspection methods and its own bare,
|
|
137
|
+
public `invoke`.
|
|
138
|
+
|
|
139
|
+
`create`/`connect`/`connect_or_create` set `ole_created?`/`ole_created` on
|
|
140
|
+
the `Proxy` they return: `true`/`True` if a new instance was created,
|
|
141
|
+
`false`/`False` if an existing one was attached to, and `nil`/`None` for
|
|
142
|
+
any object derived from another (`xl.Worksheets`, `xl.Workbooks.Add`, ...)
|
|
143
|
+
-- attach-vs-create isn't a meaningful question for those. Handy for
|
|
144
|
+
deciding whether to `Quit` on the way out:
|
|
145
|
+
|
|
146
|
+
Python:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
xl = wineole.connect_or_create('Excel.Application')
|
|
150
|
+
try:
|
|
151
|
+
...
|
|
152
|
+
finally:
|
|
153
|
+
if xl.ole_created:
|
|
154
|
+
xl.Quit()
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Ruby:
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
xl = WineOLE.connect_or_create('Excel.Application')
|
|
161
|
+
begin
|
|
162
|
+
...
|
|
163
|
+
ensure
|
|
164
|
+
xl.Quit if xl.ole_created?
|
|
165
|
+
end
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`Client.open`/`wineole`'s equivalents reuse an already-running bridge on the
|
|
169
|
+
given port, or spawn one and wait for it to come up -- either language's
|
|
170
|
+
client can talk to a bridge the other one started. `client.close()`/
|
|
171
|
+
`client.close` (or the implicit default's own eventual cleanup) releases the
|
|
172
|
+
session; the bridge automatically frees everything the connection owned (COM
|
|
173
|
+
objects included) once it sees the connection drop, and shuts down after 30
|
|
174
|
+
minutes of no active connections.
|
|
175
|
+
|
|
176
|
+
## Remote bridges
|
|
177
|
+
|
|
178
|
+
`wineole-bridge` binds to `127.0.0.1` only by default, so it's unreachable
|
|
179
|
+
over the network unless you opt in. On the machine that will run the bridge:
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
WINEOLE_BIND=0.0.0.0 WINEOLE_TOKEN=<a secret> wine wineole-bridge.exe 47800
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`WINEOLE_TOKEN` is optional but strongly recommended: without it, anything
|
|
186
|
+
that can reach that port can drive COM automation on that machine. The token
|
|
187
|
+
is only required from non-loopback connections, so this doesn't change
|
|
188
|
+
anything about local (`127.0.0.1`) use.
|
|
189
|
+
|
|
190
|
+
Then, from the client, pass `host:`/`host=` and `token:`/`token=` to
|
|
191
|
+
`Client.open`:
|
|
192
|
+
|
|
193
|
+
Python:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
client = wineole.Client.open(host='192.168.1.50', port=47800, token='<a secret>')
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Ruby:
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
client = WineOLE::Client.open(host: '192.168.1.50', port: 47800, token: '<a secret>')
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`Client.open`'s auto-spawn fallback always launches the bridge on the
|
|
206
|
+
*local* machine -- it has no way to start one remotely. That's harmless when
|
|
207
|
+
the remote bridge is already running (the fast path just connects to it and
|
|
208
|
+
the spawn logic is never reached), but if it isn't running yet, the client
|
|
209
|
+
will spend the full `timeout` retrying a connection to a host nothing is
|
|
210
|
+
listening on yet, then raise, having pointlessly spawned a useless local
|
|
211
|
+
bridge in the meantime. Start the remote bridge yourself first, or pass a
|
|
212
|
+
`spawner:`/`spawner=` that raises instead of spawning locally, e.g.
|
|
213
|
+
`spawner: ->(port) { raise 'start the remote bridge manually' }`.
|
|
214
|
+
|
|
215
|
+
The wire protocol itself is plain, unencrypted TCP -- the token authenticates
|
|
216
|
+
but does not encrypt. Tunnel through SSH or a VPN if the network between the
|
|
217
|
+
client and the bridge isn't trusted.
|
|
218
|
+
|
|
219
|
+
## Prebuilt binaries
|
|
220
|
+
|
|
221
|
+
Prebuilt `wineole-bridge` binaries are cross-compiled for three Windows
|
|
222
|
+
architectures:
|
|
223
|
+
|
|
224
|
+
- `x86_64-pc-windows-gnu` and `i686-pc-windows-gnu`, built with the widely
|
|
225
|
+
available `mingw-w64` toolchain (`apt install mingw-w64` on Debian/Ubuntu).
|
|
226
|
+
These are exercised by this project's real-Excel integration tests on every
|
|
227
|
+
change.
|
|
228
|
+
- `aarch64-pc-windows-gnullvm`, built with the
|
|
229
|
+
[`llvm-mingw`](https://github.com/mstorsjo/llvm-mingw) toolchain instead
|
|
230
|
+
(not packaged for apt — download a release tarball and put its `bin/` on
|
|
231
|
+
`PATH`; `wineole-bridge/.cargo/config.toml` points the target at
|
|
232
|
+
`aarch64-w64-mingw32-clang` from it). **This binary has never been run.**
|
|
233
|
+
This host (and every host this project has been developed on) has no way
|
|
234
|
+
to execute an ARM64 Windows binary at all -- Wine here only supports
|
|
235
|
+
x86/x86_64 execution, and there is no ARM64 CPU emulation layer (no
|
|
236
|
+
`qemu-user`, no ARM64 Wine build) to bridge that gap. The binary is a
|
|
237
|
+
structurally valid `PE32+ Aarch64` executable (verified with `file`) built
|
|
238
|
+
from the same source as the other two targets, but nothing about its
|
|
239
|
+
runtime correctness -- COM marshaling, struct layouts, calling conventions
|
|
240
|
+
under the ARM64 ABI -- has been verified. Treat it as unverified until
|
|
241
|
+
someone actually runs it on real ARM64 Wine.
|
|
242
|
+
It is also currently behind the other two: it predates the
|
|
243
|
+
`connect_or_create` RPC and will reject that call, so
|
|
244
|
+
`connect_or_create`/`ole_created?`/`ole_created` do not work on ARM64
|
|
245
|
+
until someone rebuilds it with `llvm-mingw` on `PATH`.
|
|
246
|
+
|
|
247
|
+
On an unsupported host architecture both clients raise an explicit "no
|
|
248
|
+
prebuilt wineole-bridge binary for host architecture" error listing what is
|
|
249
|
+
available.
|
|
250
|
+
|
|
251
|
+
## Repository layout
|
|
252
|
+
|
|
253
|
+
`wineole-bridge/` is the shared Rust core: it builds the `wineole-bridge`
|
|
254
|
+
binaries and holds the prebuilt output in `wineole-bridge/dist/`. Each
|
|
255
|
+
language client lives in its own self-contained package directory under
|
|
256
|
+
`bindings/<lang>/` (`bindings/python/`, `bindings/ruby/`), so it can be built
|
|
257
|
+
and published independently. Because both RubyGems and setuptools reject
|
|
258
|
+
package file references that escape the package directory with `../`, each
|
|
259
|
+
binding instead contains a relative symlink back to `wineole-bridge/dist/`
|
|
260
|
+
(`bindings/python/wineole/dist` and `bindings/ruby/wineole-bridge-dist`) and
|
|
261
|
+
lists files through that symlink. These symlinks are load-bearing, not
|
|
262
|
+
decorative: if one is deleted or replaced with a real (empty) directory, the
|
|
263
|
+
gem or wheel will still build and install successfully, then fail at runtime
|
|
264
|
+
with no bridge binary present.
|
wineole-0.1.0/README.md
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# WineOLE
|
|
2
|
+
|
|
3
|
+
Drive Windows OLE/COM automation (Excel, Word, ...) from Linux, through Wine.
|
|
4
|
+
A small Windows-side bridge process (`wineole-bridge`) runs under `wine` and
|
|
5
|
+
speaks a JSON Lines RPC protocol to native Python and Ruby clients, so host
|
|
6
|
+
code can script a Windows application as if it were a local object.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
Python:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
pip install wineole
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Ruby:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
gem install wineole
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Both packages bundle the prebuilt `wineole-bridge` binaries -- no separate
|
|
23
|
+
build step, and no Windows build of Python or Ruby is needed.
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
The quickest way to get started -- open the default bridge and create a
|
|
28
|
+
COM object in one call each:
|
|
29
|
+
|
|
30
|
+
Python:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import wineole
|
|
34
|
+
|
|
35
|
+
xl = wineole.create('Excel.Application')
|
|
36
|
+
xl.Visible = True
|
|
37
|
+
xl.Workbooks().Add()
|
|
38
|
+
xl.Worksheets()[1].Range('A1').Value = 'Hello from Python'
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Ruby:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
require 'wineole'
|
|
45
|
+
|
|
46
|
+
xl = WineOLE.create('Excel.Application')
|
|
47
|
+
xl.Visible = true
|
|
48
|
+
xl.Workbooks.Add
|
|
49
|
+
xl.Worksheets[1].Range('A1').Value = 'Hello from Ruby'
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`WineOLE.create`/`wineole.create()` (create a new instance), `.connect`/
|
|
53
|
+
`connect()` (attach to an already-running instance, failing if none exists),
|
|
54
|
+
and `.connect_or_create`/`connect_or_create()` (attach if something's
|
|
55
|
+
running, otherwise create -- the classic VBScript `GetObject`-or-
|
|
56
|
+
`CreateObject` idiom) all lazily open a default bridge connection the first
|
|
57
|
+
time any of them is called, then reuse it -- no separate client variable
|
|
58
|
+
needed for the common case. `WineOLE.close`/`wineole.close()` releases that
|
|
59
|
+
implicit default early if you want to (otherwise it lives for the process's
|
|
60
|
+
lifetime).
|
|
61
|
+
|
|
62
|
+
For anything beyond the zero-config case -- a specific host/port, an auth
|
|
63
|
+
token, more than one bridge connection at once -- use `Client.open`
|
|
64
|
+
explicitly and create objects on it directly:
|
|
65
|
+
|
|
66
|
+
Python:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import wineole
|
|
70
|
+
|
|
71
|
+
client = wineole.Client.open()
|
|
72
|
+
xl = client.create('Excel.Application')
|
|
73
|
+
|
|
74
|
+
xl.Visible = True
|
|
75
|
+
xl.Workbooks().Add()
|
|
76
|
+
xl.Worksheets()[1].Range('A1').Value = 'Hello from Python'
|
|
77
|
+
|
|
78
|
+
client.close()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Ruby:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
require 'wineole'
|
|
85
|
+
|
|
86
|
+
client = WineOLE::Client.open
|
|
87
|
+
xl = client.create('Excel.Application')
|
|
88
|
+
|
|
89
|
+
xl.Visible = true
|
|
90
|
+
xl.Workbooks.Add
|
|
91
|
+
xl.Worksheets[1].Range('A1').Value = 'Hello from Ruby'
|
|
92
|
+
|
|
93
|
+
client.close
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`WineOLE.open`/`wineole.open()` sits between the two: it's `Client.open`
|
|
97
|
+
reachable without typing `Client`, and it *also* becomes the implicit
|
|
98
|
+
default, so a `WineOLE.create`/`wineole.create()` call right after uses the
|
|
99
|
+
client you just explicitly configured rather than a second, separate
|
|
100
|
+
zero-config one.
|
|
101
|
+
|
|
102
|
+
The two clients are otherwise near-identical in capability -- the extra `()`
|
|
103
|
+
in the Python version is the one real syntactic difference: Python's
|
|
104
|
+
`__getattr__` can't tell a property read from a method call apart the way
|
|
105
|
+
Ruby's `method_missing` can, so every COM member access returns a callable
|
|
106
|
+
that has to actually be called. See `bindings/ruby/examples/` and
|
|
107
|
+
`bindings/python/examples/` for a larger, visual demo of both.
|
|
108
|
+
|
|
109
|
+
### Meta-methods and the `ole_` prefix
|
|
110
|
+
|
|
111
|
+
A `Proxy` forwards any name it doesn't recognize straight to the remote COM
|
|
112
|
+
object, so its own bookkeeping methods are named with an `ole_` prefix --
|
|
113
|
+
`ole_handle`, `ole_session_id`, `ole_release`, `ole_const_load`, and
|
|
114
|
+
`ole_created?`/`ole_created` (below) -- to keep them out of the way of a
|
|
115
|
+
real COM member that happens to share the plain name. `invoke` is the one
|
|
116
|
+
deliberate exception: it's kept bare and public as an explicit escape hatch
|
|
117
|
+
for the rare case where a COM object really does define a member called
|
|
118
|
+
e.g. `ole_handle` -- `proxy.invoke('ole_handle', [], {})` reaches it
|
|
119
|
+
directly, bypassing the local method entirely. This mirrors real Ruby
|
|
120
|
+
`WIN32OLE`'s own `ole_*`-prefixed introspection methods and its own bare,
|
|
121
|
+
public `invoke`.
|
|
122
|
+
|
|
123
|
+
`create`/`connect`/`connect_or_create` set `ole_created?`/`ole_created` on
|
|
124
|
+
the `Proxy` they return: `true`/`True` if a new instance was created,
|
|
125
|
+
`false`/`False` if an existing one was attached to, and `nil`/`None` for
|
|
126
|
+
any object derived from another (`xl.Worksheets`, `xl.Workbooks.Add`, ...)
|
|
127
|
+
-- attach-vs-create isn't a meaningful question for those. Handy for
|
|
128
|
+
deciding whether to `Quit` on the way out:
|
|
129
|
+
|
|
130
|
+
Python:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
xl = wineole.connect_or_create('Excel.Application')
|
|
134
|
+
try:
|
|
135
|
+
...
|
|
136
|
+
finally:
|
|
137
|
+
if xl.ole_created:
|
|
138
|
+
xl.Quit()
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Ruby:
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
xl = WineOLE.connect_or_create('Excel.Application')
|
|
145
|
+
begin
|
|
146
|
+
...
|
|
147
|
+
ensure
|
|
148
|
+
xl.Quit if xl.ole_created?
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`Client.open`/`wineole`'s equivalents reuse an already-running bridge on the
|
|
153
|
+
given port, or spawn one and wait for it to come up -- either language's
|
|
154
|
+
client can talk to a bridge the other one started. `client.close()`/
|
|
155
|
+
`client.close` (or the implicit default's own eventual cleanup) releases the
|
|
156
|
+
session; the bridge automatically frees everything the connection owned (COM
|
|
157
|
+
objects included) once it sees the connection drop, and shuts down after 30
|
|
158
|
+
minutes of no active connections.
|
|
159
|
+
|
|
160
|
+
## Remote bridges
|
|
161
|
+
|
|
162
|
+
`wineole-bridge` binds to `127.0.0.1` only by default, so it's unreachable
|
|
163
|
+
over the network unless you opt in. On the machine that will run the bridge:
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
WINEOLE_BIND=0.0.0.0 WINEOLE_TOKEN=<a secret> wine wineole-bridge.exe 47800
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`WINEOLE_TOKEN` is optional but strongly recommended: without it, anything
|
|
170
|
+
that can reach that port can drive COM automation on that machine. The token
|
|
171
|
+
is only required from non-loopback connections, so this doesn't change
|
|
172
|
+
anything about local (`127.0.0.1`) use.
|
|
173
|
+
|
|
174
|
+
Then, from the client, pass `host:`/`host=` and `token:`/`token=` to
|
|
175
|
+
`Client.open`:
|
|
176
|
+
|
|
177
|
+
Python:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
client = wineole.Client.open(host='192.168.1.50', port=47800, token='<a secret>')
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Ruby:
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
client = WineOLE::Client.open(host: '192.168.1.50', port: 47800, token: '<a secret>')
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`Client.open`'s auto-spawn fallback always launches the bridge on the
|
|
190
|
+
*local* machine -- it has no way to start one remotely. That's harmless when
|
|
191
|
+
the remote bridge is already running (the fast path just connects to it and
|
|
192
|
+
the spawn logic is never reached), but if it isn't running yet, the client
|
|
193
|
+
will spend the full `timeout` retrying a connection to a host nothing is
|
|
194
|
+
listening on yet, then raise, having pointlessly spawned a useless local
|
|
195
|
+
bridge in the meantime. Start the remote bridge yourself first, or pass a
|
|
196
|
+
`spawner:`/`spawner=` that raises instead of spawning locally, e.g.
|
|
197
|
+
`spawner: ->(port) { raise 'start the remote bridge manually' }`.
|
|
198
|
+
|
|
199
|
+
The wire protocol itself is plain, unencrypted TCP -- the token authenticates
|
|
200
|
+
but does not encrypt. Tunnel through SSH or a VPN if the network between the
|
|
201
|
+
client and the bridge isn't trusted.
|
|
202
|
+
|
|
203
|
+
## Prebuilt binaries
|
|
204
|
+
|
|
205
|
+
Prebuilt `wineole-bridge` binaries are cross-compiled for three Windows
|
|
206
|
+
architectures:
|
|
207
|
+
|
|
208
|
+
- `x86_64-pc-windows-gnu` and `i686-pc-windows-gnu`, built with the widely
|
|
209
|
+
available `mingw-w64` toolchain (`apt install mingw-w64` on Debian/Ubuntu).
|
|
210
|
+
These are exercised by this project's real-Excel integration tests on every
|
|
211
|
+
change.
|
|
212
|
+
- `aarch64-pc-windows-gnullvm`, built with the
|
|
213
|
+
[`llvm-mingw`](https://github.com/mstorsjo/llvm-mingw) toolchain instead
|
|
214
|
+
(not packaged for apt — download a release tarball and put its `bin/` on
|
|
215
|
+
`PATH`; `wineole-bridge/.cargo/config.toml` points the target at
|
|
216
|
+
`aarch64-w64-mingw32-clang` from it). **This binary has never been run.**
|
|
217
|
+
This host (and every host this project has been developed on) has no way
|
|
218
|
+
to execute an ARM64 Windows binary at all -- Wine here only supports
|
|
219
|
+
x86/x86_64 execution, and there is no ARM64 CPU emulation layer (no
|
|
220
|
+
`qemu-user`, no ARM64 Wine build) to bridge that gap. The binary is a
|
|
221
|
+
structurally valid `PE32+ Aarch64` executable (verified with `file`) built
|
|
222
|
+
from the same source as the other two targets, but nothing about its
|
|
223
|
+
runtime correctness -- COM marshaling, struct layouts, calling conventions
|
|
224
|
+
under the ARM64 ABI -- has been verified. Treat it as unverified until
|
|
225
|
+
someone actually runs it on real ARM64 Wine.
|
|
226
|
+
It is also currently behind the other two: it predates the
|
|
227
|
+
`connect_or_create` RPC and will reject that call, so
|
|
228
|
+
`connect_or_create`/`ole_created?`/`ole_created` do not work on ARM64
|
|
229
|
+
until someone rebuilds it with `llvm-mingw` on `PATH`.
|
|
230
|
+
|
|
231
|
+
On an unsupported host architecture both clients raise an explicit "no
|
|
232
|
+
prebuilt wineole-bridge binary for host architecture" error listing what is
|
|
233
|
+
available.
|
|
234
|
+
|
|
235
|
+
## Repository layout
|
|
236
|
+
|
|
237
|
+
`wineole-bridge/` is the shared Rust core: it builds the `wineole-bridge`
|
|
238
|
+
binaries and holds the prebuilt output in `wineole-bridge/dist/`. Each
|
|
239
|
+
language client lives in its own self-contained package directory under
|
|
240
|
+
`bindings/<lang>/` (`bindings/python/`, `bindings/ruby/`), so it can be built
|
|
241
|
+
and published independently. Because both RubyGems and setuptools reject
|
|
242
|
+
package file references that escape the package directory with `../`, each
|
|
243
|
+
binding instead contains a relative symlink back to `wineole-bridge/dist/`
|
|
244
|
+
(`bindings/python/wineole/dist` and `bindings/ruby/wineole-bridge-dist`) and
|
|
245
|
+
lists files through that symlink. These symlinks are load-bearing, not
|
|
246
|
+
decorative: if one is deleted or replaced with a real (empty) directory, the
|
|
247
|
+
gem or wheel will still build and install successfully, then fail at runtime
|
|
248
|
+
with no bridge binary present.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "wineole"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Drive Win32OLE/COM automation of Wine-hosted Windows apps from Python"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Operating System :: POSIX :: Linux",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://github.com/firelzrd/wineole"
|
|
21
|
+
Repository = "https://github.com/firelzrd/wineole"
|
|
22
|
+
Issues = "https://github.com/firelzrd/wineole/issues"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.package-data]
|
|
25
|
+
wineole = ["dist/**/*"]
|
wineole-0.1.0/setup.cfg
ADDED