pynect 0.0.0-stage → 0.2.0
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 +21 -0
- package/README.md +478 -2
- package/SECURITY.md +34 -0
- package/binding.gyp +60 -0
- package/dist/cli/codegen.d.ts +3 -0
- package/dist/cli/codegen.d.ts.map +1 -0
- package/dist/cli/codegen.js +543 -0
- package/dist/cli/codegen.js.map +1 -0
- package/dist/cli/sync.d.ts +3 -0
- package/dist/cli/sync.d.ts.map +1 -0
- package/dist/cli/sync.js +48 -0
- package/dist/cli/sync.js.map +1 -0
- package/dist/config/project.d.ts +57 -0
- package/dist/config/project.d.ts.map +1 -0
- package/dist/config/project.js +176 -0
- package/dist/config/project.js.map +1 -0
- package/dist/core/IsolatedWorker.d.ts +39 -0
- package/dist/core/IsolatedWorker.d.ts.map +1 -0
- package/dist/core/IsolatedWorker.js +178 -0
- package/dist/core/IsolatedWorker.js.map +1 -0
- package/dist/core/PyCallable.d.ts +67 -0
- package/dist/core/PyCallable.d.ts.map +1 -0
- package/dist/core/PyCallable.js +104 -0
- package/dist/core/PyCallable.js.map +1 -0
- package/dist/core/PyContext.d.ts +76 -0
- package/dist/core/PyContext.d.ts.map +1 -0
- package/dist/core/PyContext.js +239 -0
- package/dist/core/PyContext.js.map +1 -0
- package/dist/core/PyIterator.d.ts +84 -0
- package/dist/core/PyIterator.d.ts.map +1 -0
- package/dist/core/PyIterator.js +243 -0
- package/dist/core/PyIterator.js.map +1 -0
- package/dist/core/PyModule.d.ts +53 -0
- package/dist/core/PyModule.d.ts.map +1 -0
- package/dist/core/PyModule.js +162 -0
- package/dist/core/PyModule.js.map +1 -0
- package/dist/core/PyProxy.d.ts +75 -0
- package/dist/core/PyProxy.d.ts.map +1 -0
- package/dist/core/PyProxy.js +563 -0
- package/dist/core/PyProxy.js.map +1 -0
- package/dist/core/PyRuntime.d.ts +112 -0
- package/dist/core/PyRuntime.d.ts.map +1 -0
- package/dist/core/PyRuntime.js +546 -0
- package/dist/core/PyRuntime.js.map +1 -0
- package/dist/env/PackageInstaller.d.ts +232 -0
- package/dist/env/PackageInstaller.d.ts.map +1 -0
- package/dist/env/PackageInstaller.js +628 -0
- package/dist/env/PackageInstaller.js.map +1 -0
- package/dist/env/ProjectEnvironment.d.ts +63 -0
- package/dist/env/ProjectEnvironment.d.ts.map +1 -0
- package/dist/env/ProjectEnvironment.js +379 -0
- package/dist/env/ProjectEnvironment.js.map +1 -0
- package/dist/env/PyfigAdapter.d.ts +32 -0
- package/dist/env/PyfigAdapter.d.ts.map +1 -0
- package/dist/env/PyfigAdapter.js +10 -0
- package/dist/env/PyfigAdapter.js.map +1 -0
- package/dist/env/PythonDetector.d.ts +48 -0
- package/dist/env/PythonDetector.d.ts.map +1 -0
- package/dist/env/PythonDetector.js +182 -0
- package/dist/env/PythonDetector.js.map +1 -0
- package/dist/env/PythonProvisioner.d.ts +16 -0
- package/dist/env/PythonProvisioner.d.ts.map +1 -0
- package/dist/env/PythonProvisioner.js +42 -0
- package/dist/env/PythonProvisioner.js.map +1 -0
- package/dist/env/VenvManager.d.ts +55 -0
- package/dist/env/VenvManager.d.ts.map +1 -0
- package/dist/env/VenvManager.js +153 -0
- package/dist/env/VenvManager.js.map +1 -0
- package/dist/env/executables.d.ts +3 -0
- package/dist/env/executables.d.ts.map +1 -0
- package/dist/env/executables.js +14 -0
- package/dist/env/executables.js.map +1 -0
- package/dist/index.d.ts +182 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +393 -0
- package/dist/index.js.map +1 -0
- package/dist/native/NativeAddonLoader.d.ts +11 -0
- package/dist/native/NativeAddonLoader.d.ts.map +1 -0
- package/dist/native/NativeAddonLoader.js +75 -0
- package/dist/native/NativeAddonLoader.js.map +1 -0
- package/dist/plugins/Plugin.interface.d.ts +41 -0
- package/dist/plugins/Plugin.interface.d.ts.map +1 -0
- package/dist/plugins/Plugin.interface.js +12 -0
- package/dist/plugins/Plugin.interface.js.map +1 -0
- package/dist/plugins/PluginManager.d.ts +26 -0
- package/dist/plugins/PluginManager.d.ts.map +1 -0
- package/dist/plugins/PluginManager.js +174 -0
- package/dist/plugins/PluginManager.js.map +1 -0
- package/dist/plugins/builtin/NumpyPlugin.d.ts +17 -0
- package/dist/plugins/builtin/NumpyPlugin.d.ts.map +1 -0
- package/dist/plugins/builtin/NumpyPlugin.js +41 -0
- package/dist/plugins/builtin/NumpyPlugin.js.map +1 -0
- package/dist/plugins/builtin/PandasPlugin.d.ts +19 -0
- package/dist/plugins/builtin/PandasPlugin.d.ts.map +1 -0
- package/dist/plugins/builtin/PandasPlugin.js +57 -0
- package/dist/plugins/builtin/PandasPlugin.js.map +1 -0
- package/dist/plugins/builtin/TorchPlugin.d.ts +23 -0
- package/dist/plugins/builtin/TorchPlugin.d.ts.map +1 -0
- package/dist/plugins/builtin/TorchPlugin.js +50 -0
- package/dist/plugins/builtin/TorchPlugin.js.map +1 -0
- package/dist/plugins/index.d.ts +7 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +12 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/serialization/DataFrameBridge.d.ts +141 -0
- package/dist/serialization/DataFrameBridge.d.ts.map +1 -0
- package/dist/serialization/DataFrameBridge.js +355 -0
- package/dist/serialization/DataFrameBridge.js.map +1 -0
- package/dist/serialization/MsgPackSerializer.d.ts +45 -0
- package/dist/serialization/MsgPackSerializer.d.ts.map +1 -0
- package/dist/serialization/MsgPackSerializer.js +242 -0
- package/dist/serialization/MsgPackSerializer.js.map +1 -0
- package/dist/serialization/NumpyBridge.d.ts +96 -0
- package/dist/serialization/NumpyBridge.d.ts.map +1 -0
- package/dist/serialization/NumpyBridge.js +323 -0
- package/dist/serialization/NumpyBridge.js.map +1 -0
- package/dist/serialization/Serializer.d.ts +78 -0
- package/dist/serialization/Serializer.d.ts.map +1 -0
- package/dist/serialization/Serializer.js +281 -0
- package/dist/serialization/Serializer.js.map +1 -0
- package/dist/types/addon.d.ts +131 -0
- package/dist/types/addon.d.ts.map +1 -0
- package/dist/types/addon.js +32 -0
- package/dist/types/addon.js.map +1 -0
- package/dist/types/config.d.ts +233 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +60 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +12 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/python.d.ts +235 -0
- package/dist/types/python.d.ts.map +1 -0
- package/dist/types/python.js +7 -0
- package/dist/types/python.js.map +1 -0
- package/dist/utils/ErrorTranslator.d.ts +83 -0
- package/dist/utils/ErrorTranslator.d.ts.map +1 -0
- package/dist/utils/ErrorTranslator.js +210 -0
- package/dist/utils/ErrorTranslator.js.map +1 -0
- package/dist/utils/Locking.d.ts +3 -0
- package/dist/utils/Locking.d.ts.map +1 -0
- package/dist/utils/Locking.js +10 -0
- package/dist/utils/Locking.js.map +1 -0
- package/dist/utils/Logger.d.ts +27 -0
- package/dist/utils/Logger.d.ts.map +1 -0
- package/dist/utils/Logger.js +115 -0
- package/dist/utils/Logger.js.map +1 -0
- package/dist/utils/MemoryMonitor.d.ts +43 -0
- package/dist/utils/MemoryMonitor.d.ts.map +1 -0
- package/dist/utils/MemoryMonitor.js +143 -0
- package/dist/utils/MemoryMonitor.js.map +1 -0
- package/package.json +114 -4
- package/python/ast_inspector.py +248 -0
- package/python/error_handler.py +432 -0
- package/python/isolated_worker.py +94 -0
- package/python/pynect_dispatcher.py +382 -0
- package/python/pynect_runtime.py +574 -0
- package/python/serializer.py +373 -0
- package/python/type_inspector.py +315 -0
- package/scripts/build-native.js +50 -0
- package/scripts/cli.js +21 -0
- package/scripts/ensure-native.js +85 -0
- package/scripts/postinstall.js +40 -0
- package/scripts/pyfig.js +113 -0
- package/scripts/python-provisioner.js +23 -0
- package/scripts/smoke-python-boundary.js +103 -0
- package/src/native/NativeAddonLoader.ts +98 -0
- package/src/native/gil_guard.cpp +25 -0
- package/src/native/gil_guard.h +174 -0
- package/src/native/pynect_addon.cpp +942 -0
- package/src/native/python_bridge.cpp +904 -0
- package/src/native/python_bridge.h +263 -0
- package/src/native/thread_pool.cpp +375 -0
- package/src/native/thread_pool.h +185 -0
- package/src/native/type_converter.cpp +906 -0
- package/src/native/type_converter.h +271 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
const { ensurePynectPython } = require('./pyfig');
|
|
7
|
+
|
|
8
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
9
|
+
|
|
10
|
+
async function main() {
|
|
11
|
+
const projectRoot = process.env.PYNECT_PROJECT_ROOT || process.env.INIT_CWD || ROOT;
|
|
12
|
+
const { python } = await ensurePynectPython({
|
|
13
|
+
projectRoot,
|
|
14
|
+
pythonPath: process.env.PYNECT_PYTHON,
|
|
15
|
+
});
|
|
16
|
+
const embedding = python.embedding;
|
|
17
|
+
const nodeGyp = require.resolve('node-gyp/bin/node-gyp.js');
|
|
18
|
+
const mode = process.argv.includes('--debug') ? '--debug' : '--release';
|
|
19
|
+
const args = [nodeGyp, 'rebuild', mode];
|
|
20
|
+
if (process.argv.includes('--verbose')) args.push('--verbose');
|
|
21
|
+
console.log(`[pynect build] Python ${python.version}: ${python.executable}`);
|
|
22
|
+
const env = {
|
|
23
|
+
...process.env,
|
|
24
|
+
npm_config_python: python.executable,
|
|
25
|
+
PYTHON: python.executable,
|
|
26
|
+
PYNECT_PYTHON_INCLUDE_DIR: embedding.includeDirectory,
|
|
27
|
+
PYNECT_PYTHON_LIBRARY: embedding.libraryPath,
|
|
28
|
+
PYNECT_PYTHON_LIBRARY_DIR: embedding.libraryDirectory,
|
|
29
|
+
PATH: [embedding.libraryDirectory, path.dirname(python.executable), process.env.PATH || '']
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.join(path.delimiter),
|
|
32
|
+
};
|
|
33
|
+
const result = spawnSync(process.execPath, args, { cwd: ROOT, env, stdio: 'inherit', windowsHide: true });
|
|
34
|
+
if (result.status !== 0) throw new Error(`node-gyp failed with exit code ${result.status}`);
|
|
35
|
+
const target = path.join(ROOT, 'build', mode === '--debug' ? 'Debug' : 'Release', 'pynect_addon.node');
|
|
36
|
+
if (!fs.existsSync(target)) throw new Error(`Native addon was not created at ${target}`);
|
|
37
|
+
fs.writeFileSync(
|
|
38
|
+
path.join(ROOT, 'build', 'pynect-python.json'),
|
|
39
|
+
`${JSON.stringify({
|
|
40
|
+
schemaVersion: 1,
|
|
41
|
+
pythonExecutable: path.resolve(python.executable),
|
|
42
|
+
pythonVersion: python.version,
|
|
43
|
+
pythonLibrary: path.resolve(embedding.libraryPath),
|
|
44
|
+
buildType: mode === '--debug' ? 'Debug' : 'Release',
|
|
45
|
+
}, null, 2)}\n`,
|
|
46
|
+
);
|
|
47
|
+
console.log(`[pynect build] Built ${path.relative(ROOT, target)}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
main().catch(error => { console.error(`[pynect build] ${error.message}`); process.exitCode = 1; });
|
package/scripts/cli.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { Command } = require('commander');
|
|
5
|
+
const packageJson = require('../package.json');
|
|
6
|
+
const { createCodegenCommand } = require('../dist/cli/codegen');
|
|
7
|
+
const { createSyncCommand } = require('../dist/cli/sync');
|
|
8
|
+
|
|
9
|
+
const program = new Command()
|
|
10
|
+
.name('pynect')
|
|
11
|
+
.description('Generate types and manage Pynect projects.')
|
|
12
|
+
.version(packageJson.version)
|
|
13
|
+
.showHelpAfterError();
|
|
14
|
+
|
|
15
|
+
program.addCommand(createCodegenCommand());
|
|
16
|
+
program.addCommand(createSyncCommand());
|
|
17
|
+
|
|
18
|
+
program.parseAsync(process.argv).catch(error => {
|
|
19
|
+
console.error(`pynect: ${error.message}`);
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
8
|
+
const BUILD_DIRECTORY = path.join(ROOT, 'build');
|
|
9
|
+
const METADATA = path.join(BUILD_DIRECTORY, 'pynect-python.json');
|
|
10
|
+
|
|
11
|
+
function normalize(value) {
|
|
12
|
+
const resolved = path.resolve(value);
|
|
13
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function matchingAddonPath() {
|
|
17
|
+
const selected = process.env.PYNECT_PYTHON;
|
|
18
|
+
if (!selected || !fs.existsSync(METADATA)) return null;
|
|
19
|
+
try {
|
|
20
|
+
const metadata = JSON.parse(fs.readFileSync(METADATA, 'utf8'));
|
|
21
|
+
if (
|
|
22
|
+
metadata.schemaVersion !== 1 ||
|
|
23
|
+
normalize(metadata.pythonExecutable) !== normalize(selected)
|
|
24
|
+
) return null;
|
|
25
|
+
return path.join(
|
|
26
|
+
BUILD_DIRECTORY,
|
|
27
|
+
metadata.buildType === 'Debug' ? 'Debug' : 'Release',
|
|
28
|
+
'pynect_addon.node',
|
|
29
|
+
);
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function loads() {
|
|
36
|
+
const addon = matchingAddonPath();
|
|
37
|
+
if (!addon || !fs.existsSync(addon)) return false;
|
|
38
|
+
try {
|
|
39
|
+
require(addon);
|
|
40
|
+
return true;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function build() {
|
|
47
|
+
console.log(`[pynect native] Building with active runtime ${process.execPath}`);
|
|
48
|
+
const result = spawnSync(process.execPath, [path.join(__dirname, 'build-native.js')], {
|
|
49
|
+
cwd: ROOT,
|
|
50
|
+
env: process.env,
|
|
51
|
+
stdio: 'inherit',
|
|
52
|
+
windowsHide: true,
|
|
53
|
+
});
|
|
54
|
+
if (result.error) throw result.error;
|
|
55
|
+
if (result.status !== 0) {
|
|
56
|
+
throw new Error(`Native addon build failed with exit code ${result.status}.`);
|
|
57
|
+
}
|
|
58
|
+
if (!loads()) throw new Error('Native addon did not load after it was built.');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function main() {
|
|
62
|
+
const force = process.env.PYNECT_FORCE_BUILD === '1';
|
|
63
|
+
if (!force && loads()) return;
|
|
64
|
+
if (process.env.PYNECT_SKIP_BUILD === '1') {
|
|
65
|
+
throw new Error('Native addon is unavailable and building is disabled by PYNECT_SKIP_BUILD=1.');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fs.mkdirSync(BUILD_DIRECTORY, { recursive: true });
|
|
69
|
+
const { withLock } = await import('loqkit');
|
|
70
|
+
await withLock(
|
|
71
|
+
{ buildDirectory: BUILD_DIRECTORY },
|
|
72
|
+
async () => build(),
|
|
73
|
+
async () => force || !loads(),
|
|
74
|
+
{
|
|
75
|
+
key: `pynect-native:${normalize(BUILD_DIRECTORY)}`,
|
|
76
|
+
name: 'Pynect native build',
|
|
77
|
+
namespace: 'pynect',
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
main().catch(error => {
|
|
83
|
+
console.error(`[pynect native] ${error.message}`);
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
function disabled(value) {
|
|
7
|
+
return value !== undefined && /^(1|true|yes|on)$/i.test(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function main() {
|
|
11
|
+
if (disabled(process.env.PYNECT_SKIP_POSTINSTALL)) return;
|
|
12
|
+
|
|
13
|
+
const projectRoot = path.resolve(
|
|
14
|
+
process.env.INIT_CWD || process.env.npm_config_local_prefix || process.cwd(),
|
|
15
|
+
);
|
|
16
|
+
const environmentModule = path.join(__dirname, '..', 'dist', 'env', 'ProjectEnvironment.js');
|
|
17
|
+
|
|
18
|
+
// Published packages contain dist. A source checkout may install dependencies
|
|
19
|
+
// before its first TypeScript build, so its normal verify/build flow owns setup.
|
|
20
|
+
if (!fs.existsSync(environmentModule)) return;
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
createProjectPlan,
|
|
24
|
+
requiresProjectSync,
|
|
25
|
+
syncProject,
|
|
26
|
+
} = require(environmentModule);
|
|
27
|
+
const plan = createProjectPlan({ projectRoot });
|
|
28
|
+
if (!requiresProjectSync(plan)) return;
|
|
29
|
+
|
|
30
|
+
const result = await syncProject({ projectRoot });
|
|
31
|
+
console.log(
|
|
32
|
+
`[pynect] Synchronized ${result.plan.requirements.length} Python requirement(s) ` +
|
|
33
|
+
`with uv in ${path.dirname(result.plan.stateFile)}.`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
main().catch(error => {
|
|
38
|
+
console.error(`[pynect] Postinstall synchronization failed: ${error.message}`);
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
});
|
package/scripts/pyfig.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
const REQUIRED_PYFIG_FUNCTIONS = [
|
|
7
|
+
'createPyfig',
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
function loadPyfig() {
|
|
11
|
+
const configured = process.env.PYNECT_PYFIG_PATH;
|
|
12
|
+
if (configured) {
|
|
13
|
+
return requireCompatiblePyfig(path.resolve(configured), 'PYNECT_PYFIG_PATH');
|
|
14
|
+
}
|
|
15
|
+
let packageError;
|
|
16
|
+
try {
|
|
17
|
+
return requireCompatiblePyfig('pyfig', 'installed pyfig package');
|
|
18
|
+
} catch (error) {
|
|
19
|
+
packageError = error;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// During coordinated source development, a published but older package can
|
|
23
|
+
// resolve successfully. Prefer the sibling build when its API is compatible.
|
|
24
|
+
const sibling = path.resolve(__dirname, '..', '..', 'pyfig', 'dist', 'cjs', 'index.js');
|
|
25
|
+
if (fs.existsSync(sibling)) {
|
|
26
|
+
return requireCompatiblePyfig(sibling, 'sibling pyfig build');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const detail = packageError instanceof Error ? ` ${packageError.message}` : '';
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Pynect requires a Pyfig release with the venv and spawn API.${detail}`,
|
|
32
|
+
{ cause: packageError },
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function requireCompatiblePyfig(specifier, label) {
|
|
37
|
+
const candidate = require(specifier);
|
|
38
|
+
const missing = REQUIRED_PYFIG_FUNCTIONS.filter(name => typeof candidate[name] !== 'function');
|
|
39
|
+
if (missing.length > 0) {
|
|
40
|
+
throw new Error(`${label} is missing required exports: ${missing.join(', ')}.`);
|
|
41
|
+
}
|
|
42
|
+
return candidate;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseBoolean(value, fallback) {
|
|
46
|
+
if (value === undefined) return fallback;
|
|
47
|
+
if (/^(1|true|yes|on)$/i.test(value)) return true;
|
|
48
|
+
if (/^(0|false|no|off)$/i.test(value)) return false;
|
|
49
|
+
throw new TypeError(`Invalid boolean value: ${value}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function projectRoot(explicit) {
|
|
53
|
+
const hint = explicit || process.env.PYNECT_PROJECT_ROOT || process.env.INIT_CWD;
|
|
54
|
+
return hint ? path.resolve(hint) : loadPyfig().resolveProjectRoot(process.cwd());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function pynectPyfigOptions(options = {}) {
|
|
58
|
+
const pyfig = loadPyfig();
|
|
59
|
+
const installPython = options.autoInstallPython ?? parseBoolean(
|
|
60
|
+
process.env.PYNECT_INSTALL_PYTHON ?? process.env.PYNECT_AUTO_INSTALL,
|
|
61
|
+
true,
|
|
62
|
+
);
|
|
63
|
+
return {
|
|
64
|
+
projectRoot: projectRoot(options.projectRoot),
|
|
65
|
+
python: options.pythonPath || process.env.PYNECT_PYTHON || '',
|
|
66
|
+
pythonRange: options.pythonRange || process.env.PYNECT_REQUIRES_PYTHON ||
|
|
67
|
+
pyfig.DEFAULT_PYTHON_RANGE,
|
|
68
|
+
managedPython: options.managedPython || process.env.PYNECT_MANAGED_PYTHON ||
|
|
69
|
+
process.env.PYNECT_PYTHON_VERSION || pyfig.DEFAULT_MANAGED_PYTHON,
|
|
70
|
+
uvVersion: options.uvVersion || process.env.PYNECT_UV_VERSION || pyfig.DEFAULT_UV_VERSION,
|
|
71
|
+
requireEmbedding: true,
|
|
72
|
+
requireGil: true,
|
|
73
|
+
installPython,
|
|
74
|
+
installUv: options.autoInstallUv ?? parseBoolean(process.env.PYNECT_INSTALL_UV, true),
|
|
75
|
+
createVirtualEnvironment: options.autoCreateVirtualEnvironment ??
|
|
76
|
+
parseBoolean(process.env.PYNECT_CREATE_VIRTUAL_ENVIRONMENT, true),
|
|
77
|
+
env: options.env ? { ...process.env, ...options.env } : process.env,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function assertEmbeddable(python) {
|
|
82
|
+
if (!python.embedding) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Python ${python.version} at ${python.executable} is missing Python.h or its CPython ` +
|
|
85
|
+
'embedding library.',
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return python;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function createPynectPyfig(options = {}) {
|
|
92
|
+
return loadPyfig().createPyfig(pynectPyfigOptions(options));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function ensurePynectPython(options = {}) {
|
|
96
|
+
const client = createPynectPyfig(options);
|
|
97
|
+
const python = assertEmbeddable(await client.pythonInfo());
|
|
98
|
+
return { client, python };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function ensurePynectEnvironment(options = {}) {
|
|
102
|
+
const client = createPynectPyfig(options);
|
|
103
|
+
const environment = await client.prepare();
|
|
104
|
+
assertEmbeddable(environment.python);
|
|
105
|
+
return { client, environment };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = {
|
|
109
|
+
createPynectPyfig,
|
|
110
|
+
ensurePynectEnvironment,
|
|
111
|
+
ensurePynectPython,
|
|
112
|
+
projectRoot,
|
|
113
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { ensurePynectPython } = require('./pyfig');
|
|
4
|
+
|
|
5
|
+
async function ensurePython(options = {}) {
|
|
6
|
+
const { python } = await ensurePynectPython({
|
|
7
|
+
autoInstallPython: options.autoInstall,
|
|
8
|
+
managedPython: options.version,
|
|
9
|
+
pythonPath: options.pythonPath,
|
|
10
|
+
projectRoot: options.projectRoot,
|
|
11
|
+
});
|
|
12
|
+
return python;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function detectPython(explicit) {
|
|
16
|
+
try {
|
|
17
|
+
return await ensurePython({ autoInstall: false, pythonPath: explicit });
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { detectPython, ensurePython };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { ensurePynectEnvironment } = require('./pyfig');
|
|
5
|
+
|
|
6
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
const { client, environment } = await ensurePynectEnvironment({ projectRoot: ROOT });
|
|
10
|
+
const { python } = environment;
|
|
11
|
+
const expected = normalize(python.executable);
|
|
12
|
+
const pynect = require('../dist');
|
|
13
|
+
|
|
14
|
+
const detector = new pynect.PythonDetector({ projectRoot: ROOT });
|
|
15
|
+
const detected = await detector.detect();
|
|
16
|
+
assertSamePython(detected.executable, expected, 'PythonDetector');
|
|
17
|
+
const probed = detector.probe(detected.executable);
|
|
18
|
+
if (!probed) throw new Error('PythonDetector could not probe the selected repository Python.');
|
|
19
|
+
assertSamePython(probed.executable, expected, 'PythonDetector.probe');
|
|
20
|
+
|
|
21
|
+
const selected = client.pythonSync(
|
|
22
|
+
['-c', 'import sys; print(sys.executable)'],
|
|
23
|
+
{ encoding: 'utf8' },
|
|
24
|
+
);
|
|
25
|
+
assertSuccess(selected, 'PyfigClient.pythonSync');
|
|
26
|
+
assertSamePython(selected.stdout.trim(), expected, 'PyfigClient.pythonSync');
|
|
27
|
+
|
|
28
|
+
const pip = client.pipSync(['list', '--format', 'json'], { encoding: 'utf8' });
|
|
29
|
+
assertSuccess(pip, 'PyfigClient.pipSync');
|
|
30
|
+
|
|
31
|
+
const uvPip = client.uvSync(
|
|
32
|
+
['pip', 'list', '--format', 'json'],
|
|
33
|
+
{ encoding: 'utf8' },
|
|
34
|
+
);
|
|
35
|
+
assertSuccess(uvPip, 'PyfigClient.uvSync pip');
|
|
36
|
+
|
|
37
|
+
const inspector = client.pythonSync(
|
|
38
|
+
[path.join(ROOT, 'python', 'type_inspector.py'), 'json'],
|
|
39
|
+
{ encoding: 'utf8' },
|
|
40
|
+
);
|
|
41
|
+
assertSuccess(inspector, 'codegen inspector');
|
|
42
|
+
JSON.parse(inspector.stdout);
|
|
43
|
+
|
|
44
|
+
const worker = client.pythonSync(
|
|
45
|
+
[path.join(ROOT, 'python', 'isolated_worker.py')],
|
|
46
|
+
{
|
|
47
|
+
encoding: 'utf8',
|
|
48
|
+
input: `${JSON.stringify({
|
|
49
|
+
id: 1,
|
|
50
|
+
module: 'os',
|
|
51
|
+
function: 'getenv',
|
|
52
|
+
args: ['VIRTUAL_ENV'],
|
|
53
|
+
kwargs: {},
|
|
54
|
+
})}\n`,
|
|
55
|
+
env: {
|
|
56
|
+
...process.env,
|
|
57
|
+
PYTHONPATH: path.join(ROOT, 'python'),
|
|
58
|
+
PYNECT_DISPATCHER_CONFIG: '{}',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
assertSuccess(worker, 'isolated worker');
|
|
63
|
+
const workerResult = JSON.parse(worker.stdout.trim());
|
|
64
|
+
if (!workerResult.ok || normalize(workerResult.result) !== normalize(python.virtualEnvironment)) {
|
|
65
|
+
throw new Error('The isolated worker did not use the Pyfig virtual environment.');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await pynect.init({
|
|
69
|
+
projectRoot: ROOT,
|
|
70
|
+
allowUnsafeCode: true,
|
|
71
|
+
logLevel: 'error',
|
|
72
|
+
});
|
|
73
|
+
try {
|
|
74
|
+
const embedded = await pynect.py.eval('__import__("sys").executable');
|
|
75
|
+
assertSamePython(embedded, expected, 'embedded CPython');
|
|
76
|
+
} finally {
|
|
77
|
+
await pynect.shutdown();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log(`Python boundary verified: ${python.executable}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function assertSuccess(result, label) {
|
|
84
|
+
if (result.status !== 0 || result.error) {
|
|
85
|
+
throw new Error(`${label} failed: ${String(result.stderr || result.error?.message || result.status)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function assertSamePython(actual, expected, label) {
|
|
90
|
+
if (normalize(actual) !== expected) {
|
|
91
|
+
throw new Error(`${label} used ${actual}; expected ${expected}.`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normalize(value) {
|
|
96
|
+
const resolved = path.resolve(String(value || ''));
|
|
97
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
main().catch(error => {
|
|
101
|
+
console.error(`[pynect smoke] ${error.message}`);
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { spawnSync } from 'child_process';
|
|
4
|
+
import type { NativeAddon } from '../types/addon';
|
|
5
|
+
|
|
6
|
+
export interface NativeAddonBuildOptions {
|
|
7
|
+
projectRoot: string;
|
|
8
|
+
pythonExecutable: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Load an addon built for the selected Python, compiling it through the active
|
|
13
|
+
* JavaScript runtime when no exact match exists.
|
|
14
|
+
*/
|
|
15
|
+
export function loadOrBuildNativeAddon(options: NativeAddonBuildOptions): NativeAddon {
|
|
16
|
+
let initialError: unknown = new Error('Native rebuild explicitly requested.');
|
|
17
|
+
if (process.env['PYNECT_FORCE_BUILD'] !== '1') {
|
|
18
|
+
try {
|
|
19
|
+
return loadMatchingLocalAddon(options);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
initialError = error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const packageRoot = path.resolve(__dirname, '..', '..');
|
|
26
|
+
const script = path.join(packageRoot, 'scripts', 'ensure-native.js');
|
|
27
|
+
const result = spawnSync(process.execPath, [script], {
|
|
28
|
+
cwd: packageRoot,
|
|
29
|
+
env: {
|
|
30
|
+
...process.env,
|
|
31
|
+
PYNECT_PROJECT_ROOT: options.projectRoot,
|
|
32
|
+
PYNECT_PYTHON: options.pythonExecutable,
|
|
33
|
+
},
|
|
34
|
+
stdio: 'inherit',
|
|
35
|
+
windowsHide: true,
|
|
36
|
+
});
|
|
37
|
+
if (result.error) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Could not launch the native builder with ${process.execPath}: ${result.error.message}`,
|
|
40
|
+
{ cause: initialError },
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
if (result.status !== 0) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Pynect could not load or build its native addon with ${process.execPath}. ` +
|
|
46
|
+
'Install a C++ toolchain or provide a compatible prebuilt addon.',
|
|
47
|
+
{ cause: initialError },
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
return loadMatchingLocalAddon(options);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
throw new Error('Pynect built the native addon, but the active runtime could not load it.', {
|
|
55
|
+
cause: error,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface NativeBuildMetadata {
|
|
61
|
+
schemaVersion: number;
|
|
62
|
+
pythonExecutable: string;
|
|
63
|
+
buildType: 'Debug' | 'Release';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function loadMatchingLocalAddon(options: NativeAddonBuildOptions): NativeAddon {
|
|
67
|
+
const packageRoot = path.resolve(__dirname, '..', '..');
|
|
68
|
+
const metadataPath = path.join(packageRoot, 'build', 'pynect-python.json');
|
|
69
|
+
let metadata: NativeBuildMetadata;
|
|
70
|
+
try {
|
|
71
|
+
metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8')) as NativeBuildMetadata;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
throw new Error(`Native build metadata is unavailable at ${metadataPath}.`, { cause: error });
|
|
74
|
+
}
|
|
75
|
+
if (
|
|
76
|
+
metadata.schemaVersion !== 1 ||
|
|
77
|
+
normalizePath(metadata.pythonExecutable) !== normalizePath(options.pythonExecutable)
|
|
78
|
+
) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Native addon Python ${metadata.pythonExecutable} does not match selected Python ` +
|
|
81
|
+
`${options.pythonExecutable}.`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const addonPath = path.join(
|
|
86
|
+
packageRoot,
|
|
87
|
+
'build',
|
|
88
|
+
metadata.buildType === 'Debug' ? 'Debug' : 'Release',
|
|
89
|
+
'pynect_addon.node',
|
|
90
|
+
);
|
|
91
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
92
|
+
return require(addonPath) as NativeAddon;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normalizePath(value: string): string {
|
|
96
|
+
const resolved = path.resolve(value);
|
|
97
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
98
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pynect - GIL Guard Implementation
|
|
3
|
+
* The GIL guard is header-only; this file provides explicit template
|
|
4
|
+
* instantiations and any platform-specific workarounds.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
#include "gil_guard.h"
|
|
8
|
+
|
|
9
|
+
// All implementation is in the header file (RAII classes are trivial inline).
|
|
10
|
+
// This .cpp exists to satisfy the build system and future non-inline additions.
|
|
11
|
+
|
|
12
|
+
namespace pynect {
|
|
13
|
+
|
|
14
|
+
// Verify at compile time that our RAII types are not accidentally copied
|
|
15
|
+
static_assert(!std::is_copy_constructible<GILAcquire>::value,
|
|
16
|
+
"GILAcquire must not be copy-constructible");
|
|
17
|
+
static_assert(!std::is_copy_constructible<GILRelease>::value,
|
|
18
|
+
"GILRelease must not be copy-constructible");
|
|
19
|
+
static_assert(!std::is_copy_constructible<PyRef>::value,
|
|
20
|
+
"PyRef must not be copy-constructible");
|
|
21
|
+
|
|
22
|
+
static_assert(std::is_move_constructible<PyRef>::value,
|
|
23
|
+
"PyRef must be move-constructible");
|
|
24
|
+
|
|
25
|
+
} // namespace pynect
|