@alotop/dsh-matlab-bridge 0.1.1
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.
- package/LICENSE +29 -0
- package/README.md +263 -0
- package/README.zh-CN.md +177 -0
- package/cordis.patch.yml +14 -0
- package/package.json +58 -0
- package/python/mfiles/dsh_evalbase.m +31 -0
- package/python/mfiles/dsh_figure_info.m +29 -0
- package/python/mfiles/dsh_figure_save.m +37 -0
- package/python/ml_driver.py +419 -0
- package/python/selftest.py +244 -0
- package/scripts/run-selftest.mjs +61 -0
- package/scripts/setup-engine.mjs +242 -0
- package/src/plugin.mjs +511 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Run the Python self-test with a discovered interpreter, so `npm run selftest`
|
|
4
|
+
* works the same way on every platform.
|
|
5
|
+
*
|
|
6
|
+
* The self-test needs a real MATLAB, and it starts one; it is not part of CI.
|
|
7
|
+
*
|
|
8
|
+
* npm run selftest
|
|
9
|
+
* npm run selftest -- --no-debug
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { spawnSync } from 'node:child_process'
|
|
13
|
+
import { existsSync } from 'node:fs'
|
|
14
|
+
import { dirname, join } from 'node:path'
|
|
15
|
+
import { fileURLToPath } from 'node:url'
|
|
16
|
+
import process from 'node:process'
|
|
17
|
+
|
|
18
|
+
const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
19
|
+
const SELFTEST = join(PACKAGE_ROOT, 'python', 'selftest.py')
|
|
20
|
+
const PYLIBS = join(PACKAGE_ROOT, 'python', 'pylibs')
|
|
21
|
+
|
|
22
|
+
function findPython() {
|
|
23
|
+
const dirs = (process.env.PATH || '').split(process.platform === 'win32' ? ';' : ':')
|
|
24
|
+
const suffixes = process.platform === 'win32' ? ['', '.exe'] : ['']
|
|
25
|
+
for (const name of ['python3', 'python']) {
|
|
26
|
+
for (const dir of dirs) {
|
|
27
|
+
if (dir === '') continue
|
|
28
|
+
for (const suffix of suffixes) {
|
|
29
|
+
const candidate = join(dir, name + suffix)
|
|
30
|
+
if (existsSync(candidate)) return candidate
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const python = findPython()
|
|
38
|
+
if (python === null) {
|
|
39
|
+
console.error('no Python interpreter found on PATH')
|
|
40
|
+
process.exit(1)
|
|
41
|
+
}
|
|
42
|
+
if (!existsSync(join(PYLIBS, 'matlab'))) {
|
|
43
|
+
console.error('engine runtime missing; run `npm run setup` first')
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Report the interpreter without capturing a pipe: DSH's file sandbox refuses
|
|
48
|
+
// named pipes, so `execFileSync(..., { stdio: 'pipe' })` fails with EPERM
|
|
49
|
+
// inside a DSH session. `inherit` needs no pipe.
|
|
50
|
+
console.log('python: ' + python)
|
|
51
|
+
const probe = spawnSync(python, ['--version'], { stdio: 'inherit' })
|
|
52
|
+
if (probe.status !== 0) {
|
|
53
|
+
console.error('failed to run ' + python)
|
|
54
|
+
process.exit(1)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const result = spawnSync(python, ['-W', 'ignore', SELFTEST, ...process.argv.slice(2)], {
|
|
58
|
+
stdio: 'inherit',
|
|
59
|
+
env: { ...process.env, PYTHONPATH: PYLIBS },
|
|
60
|
+
})
|
|
61
|
+
process.exit(result.status === null ? 1 : result.status)
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Lay out the MATLAB Engine for Python runtime that ships with a local MATLAB
|
|
4
|
+
* installation, so the bridge driver can `import matlab.engine`.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS STEP EXISTS
|
|
7
|
+
* The engine is not on PyPI for this MATLAB release, and it is MathWorks code
|
|
8
|
+
* that must not be redistributed. So the package ships without it and this
|
|
9
|
+
* script copies it out of the MATLAB installation the user already has.
|
|
10
|
+
*
|
|
11
|
+
* WHY NOT `pip install`
|
|
12
|
+
* The bundled engine advertises Python 3.9-3.12 and ships a stable-ABI (`abi3`)
|
|
13
|
+
* extension module. On Windows its `setup.py` also generates an `_arch.txt`
|
|
14
|
+
* that tells the engine where MATLAB's `bin` and `extern/bin` DLL directories
|
|
15
|
+
* are. A directory copy plus that generated file is exactly what the wheel
|
|
16
|
+
* would have produced, without needing a build toolchain.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* node scripts/setup-engine.mjs
|
|
20
|
+
* node scripts/setup-engine.mjs --matlab-root <matlab-root>
|
|
21
|
+
* node scripts/setup-engine.mjs --python python3.12 --force
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { spawnSync } from 'node:child_process'
|
|
25
|
+
import { cp, mkdir, rm, writeFile } from 'node:fs/promises'
|
|
26
|
+
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs'
|
|
27
|
+
import { tmpdir } from 'node:os'
|
|
28
|
+
import { dirname, join } from 'node:path'
|
|
29
|
+
import { fileURLToPath } from 'node:url'
|
|
30
|
+
import process from 'node:process'
|
|
31
|
+
|
|
32
|
+
const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
33
|
+
const DEST = join(PACKAGE_ROOT, 'python', 'pylibs')
|
|
34
|
+
|
|
35
|
+
/** MATLAB's own architecture directory names, keyed by platform. */
|
|
36
|
+
function archDirFor(platform, arch) {
|
|
37
|
+
if (platform === 'win32') return 'win64'
|
|
38
|
+
if (platform === 'linux') return 'glnxa64'
|
|
39
|
+
if (platform === 'darwin') return arch === 'arm64' ? 'maca64' : 'maci64'
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseArgs(argv) {
|
|
44
|
+
const options = { matlabRoot: null, python: null, force: false, help: false }
|
|
45
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
46
|
+
const arg = argv[i]
|
|
47
|
+
if (arg === '--help' || arg === '-h') options.help = true
|
|
48
|
+
else if (arg === '--force' || arg === '-f') options.force = true
|
|
49
|
+
else if (arg === '--matlab-root') options.matlabRoot = argv[++i] ?? null
|
|
50
|
+
else if (arg === '--python') options.python = argv[++i] ?? null
|
|
51
|
+
else if (arg.startsWith('--matlab-root=')) options.matlabRoot = arg.slice('--matlab-root='.length)
|
|
52
|
+
else if (arg.startsWith('--python=')) options.python = arg.slice('--python='.length)
|
|
53
|
+
else throw new Error('unknown argument: ' + arg)
|
|
54
|
+
}
|
|
55
|
+
return options
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Locate an executable by scanning PATH, without assuming a shell. */
|
|
59
|
+
function findOnPath(names) {
|
|
60
|
+
const dirs = (process.env.PATH || '').split(process.platform === 'win32' ? ';' : ':')
|
|
61
|
+
const suffixes = process.platform === 'win32' ? ['', '.exe', '.cmd', '.bat'] : ['']
|
|
62
|
+
for (const name of names) {
|
|
63
|
+
for (const dir of dirs) {
|
|
64
|
+
if (dir === '') continue
|
|
65
|
+
for (const suffix of suffixes) {
|
|
66
|
+
const candidate = join(dir, name + suffix)
|
|
67
|
+
if (existsSync(candidate)) return candidate
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Release directories worth probing under a platform's install root. */
|
|
75
|
+
function defaultInstallRoots() {
|
|
76
|
+
if (process.platform === 'win32') {
|
|
77
|
+
const roots = []
|
|
78
|
+
for (const base of [process.env.ProgramFiles, process.env['ProgramFiles(x86)'], 'C:\\Program Files']) {
|
|
79
|
+
if (base) roots.push(join(base, 'MATLAB'))
|
|
80
|
+
}
|
|
81
|
+
return roots
|
|
82
|
+
}
|
|
83
|
+
if (process.platform === 'darwin') return ['/Applications']
|
|
84
|
+
return ['/usr/local/MATLAB', '/opt/MATLAB', '/usr/local']
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Pick the newest release directory under one install root. Names sort
|
|
89
|
+
* lexicographically well enough for `R2023b`-style releases, and a plain
|
|
90
|
+
* version directory is accepted too.
|
|
91
|
+
*/
|
|
92
|
+
function newestRelease(root, matcher) {
|
|
93
|
+
if (!existsSync(root)) return null
|
|
94
|
+
const entries = readdirSync(root, { withFileTypes: true })
|
|
95
|
+
.filter((entry) => entry.isDirectory() && matcher(entry.name))
|
|
96
|
+
.map((entry) => entry.name)
|
|
97
|
+
.sort()
|
|
98
|
+
if (entries.length === 0) return null
|
|
99
|
+
return join(root, entries[entries.length - 1])
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function findMatlabRoot(explicit) {
|
|
103
|
+
if (explicit) return explicit
|
|
104
|
+
if (process.env.MATLAB_ROOT) return process.env.MATLAB_ROOT
|
|
105
|
+
|
|
106
|
+
// A `matlab` on PATH resolves to <root>/bin/matlab[.exe].
|
|
107
|
+
const exe = findOnPath(['matlab'])
|
|
108
|
+
if (exe !== null) {
|
|
109
|
+
const root = dirname(dirname(exe))
|
|
110
|
+
if (existsSync(join(root, 'extern', 'engines', 'python'))) return root
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const base of defaultInstallRoots()) {
|
|
114
|
+
if (process.platform === 'darwin') {
|
|
115
|
+
const app = newestRelease(base, (n) => n.startsWith('MATLAB_R'))
|
|
116
|
+
if (app !== null) return app
|
|
117
|
+
} else {
|
|
118
|
+
const release = newestRelease(base, (n) => /^R\d{4}[ab]$/.test(n) || /^\d/.test(n))
|
|
119
|
+
if (release !== null) return release
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function findPython(explicit) {
|
|
126
|
+
if (explicit) return explicit
|
|
127
|
+
return findOnPath(['python3', 'python'])
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Run a Python program and read back what it wrote to a temp file.
|
|
132
|
+
*
|
|
133
|
+
* Capturing a child's output through a PIPE is deliberately avoided: DSH runs
|
|
134
|
+
* commands inside a file sandbox that refuses named pipes, so the usual
|
|
135
|
+
* `execFileSync(..., { stdio: 'pipe' })` fails with EPERM there. `inherit`
|
|
136
|
+
* needs no pipe, and the child's own traceback still reaches the terminal.
|
|
137
|
+
*/
|
|
138
|
+
function runPython(python, program) {
|
|
139
|
+
const dir = mkdtempSync(join(tmpdir(), 'dsh-matlab-setup-'))
|
|
140
|
+
const outFile = join(dir, 'result.txt')
|
|
141
|
+
const full = [
|
|
142
|
+
program,
|
|
143
|
+
'open(' + JSON.stringify(outFile) + ', "w", encoding="utf-8").write(RESULT)',
|
|
144
|
+
].join('\n')
|
|
145
|
+
const result = spawnSync(python, ['-c', full], { stdio: 'inherit' })
|
|
146
|
+
let text = null
|
|
147
|
+
try {
|
|
148
|
+
text = readFileSync(outFile, 'utf8')
|
|
149
|
+
} catch {
|
|
150
|
+
text = null
|
|
151
|
+
}
|
|
152
|
+
rmSync(dir, { recursive: true, force: true })
|
|
153
|
+
return { status: result.status, text }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Major/minor version of an interpreter, or null when it will not run. */
|
|
157
|
+
function pythonVersion(python) {
|
|
158
|
+
const result = runPython(python, 'import sys\nRESULT = "%d.%d" % sys.version_info[:2]')
|
|
159
|
+
return result.status === 0 && result.text !== null ? result.text.trim() : null
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function main() {
|
|
163
|
+
const options = parseArgs(process.argv.slice(2))
|
|
164
|
+
if (options.help) {
|
|
165
|
+
console.log('Usage: node scripts/setup-engine.mjs [--matlab-root DIR] [--python EXE] [--force]')
|
|
166
|
+
return 0
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const arch = archDirFor(process.platform, process.arch)
|
|
170
|
+
if (arch === null) {
|
|
171
|
+
throw new Error('unsupported platform: ' + process.platform + '/' + process.arch)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const matlabRoot = findMatlabRoot(options.matlabRoot)
|
|
175
|
+
if (matlabRoot === null) {
|
|
176
|
+
throw new Error('could not find a MATLAB installation. Pass --matlab-root <dir> or set MATLAB_ROOT.')
|
|
177
|
+
}
|
|
178
|
+
const engineSource = join(matlabRoot, 'extern', 'engines', 'python', 'dist', 'matlab')
|
|
179
|
+
if (!existsSync(engineSource)) {
|
|
180
|
+
throw new Error('no bundled Python engine under ' + matlabRoot
|
|
181
|
+
+ '. MATLAB ships it in extern/engines/python; check that this install is complete.')
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (existsSync(DEST) && !options.force) {
|
|
185
|
+
console.log('engine runtime already present at ' + DEST)
|
|
186
|
+
console.log('re-run with --force to lay it out again')
|
|
187
|
+
} else {
|
|
188
|
+
await rm(DEST, { recursive: true, force: true })
|
|
189
|
+
await mkdir(DEST, { recursive: true })
|
|
190
|
+
await cp(engineSource, join(DEST, 'matlab'), { recursive: true })
|
|
191
|
+
// The four lines engine/__init__.py reads to find MATLAB's native
|
|
192
|
+
// libraries: architecture, bin, the engine's own module dir, extern/bin.
|
|
193
|
+
const archFile = [
|
|
194
|
+
arch,
|
|
195
|
+
join(matlabRoot, 'bin', arch),
|
|
196
|
+
join(DEST, 'matlab', 'engine', arch),
|
|
197
|
+
join(matlabRoot, 'extern', 'bin', arch),
|
|
198
|
+
].join('\n') + '\n'
|
|
199
|
+
await writeFile(join(DEST, 'matlab', 'engine', '_arch.txt'), archFile)
|
|
200
|
+
console.log('engine runtime -> ' + DEST)
|
|
201
|
+
console.log('matlab root -> ' + matlabRoot)
|
|
202
|
+
console.log('architecture -> ' + arch)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const python = findPython(options.python)
|
|
206
|
+
if (python === null) {
|
|
207
|
+
console.log('')
|
|
208
|
+
console.log('No Python interpreter found on PATH. The bridge will need one at run time;')
|
|
209
|
+
console.log('pass --python <exe> here, or set the pythonPath option on the preset row.')
|
|
210
|
+
return 0
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const version = pythonVersion(python)
|
|
214
|
+
console.log('')
|
|
215
|
+
console.log('python -> ' + python + (version === null ? '' : ' (' + version + ')'))
|
|
216
|
+
|
|
217
|
+
const check = runPython(python, [
|
|
218
|
+
'import sys',
|
|
219
|
+
'sys.path.insert(0, ' + JSON.stringify(DEST) + ')',
|
|
220
|
+
'import matlab.engine',
|
|
221
|
+
'RESULT = "ok"',
|
|
222
|
+
].join('\n'))
|
|
223
|
+
if (check.status !== 0 || check.text === null) {
|
|
224
|
+
console.log('import check -> FAILED (the traceback above says why)')
|
|
225
|
+
return 1
|
|
226
|
+
}
|
|
227
|
+
console.log('import check -> ' + check.text.trim())
|
|
228
|
+
if (version !== null && !/^3\.(9|1[0-3])$/.test(version)) {
|
|
229
|
+
console.log('note: the engine advertises Python 3.9-3.12; elsewhere it may need an older interpreter.')
|
|
230
|
+
}
|
|
231
|
+
console.log('')
|
|
232
|
+
console.log('Ready. Start MATLAB from a DSH session with the matlab_session tool (action="start").')
|
|
233
|
+
return 0
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
main().then(
|
|
237
|
+
(code) => { process.exitCode = code },
|
|
238
|
+
(error) => {
|
|
239
|
+
console.error('setup-engine failed: ' + String((error && error.message) || error))
|
|
240
|
+
process.exitCode = 1
|
|
241
|
+
},
|
|
242
|
+
)
|