nimporter-plus 0.0.2__py3-none-any.whl
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.
- nimporter_plus/__init__.py +21 -0
- nimporter_plus/cli.py +206 -0
- nimporter_plus/lib.py +345 -0
- nimporter_plus/nexporter.py +302 -0
- nimporter_plus/nimporter.py +288 -0
- nimporter_plus-0.0.2.dist-info/METADATA +535 -0
- nimporter_plus-0.0.2.dist-info/RECORD +10 -0
- nimporter_plus-0.0.2.dist-info/WHEEL +4 -0
- nimporter_plus-0.0.2.dist-info/entry_points.txt +3 -0
- nimporter_plus-0.0.2.dist-info/licenses/LICENSE +20 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
from icecream import ic, colorama
|
|
4
|
+
|
|
5
|
+
ic.configureOutput(
|
|
6
|
+
includeContext=True,
|
|
7
|
+
prefix=f'{colorama.Fore.CYAN}ic|{colorama.Fore.RESET} ',
|
|
8
|
+
|
|
9
|
+
# https://github.com/gruns/icecream/issues/35#issuecomment-908730426
|
|
10
|
+
outputFunction=lambda *args: print(*args)
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
ic.enabled = 'NIMPORTER_INSTRUMENT' in os.environ
|
|
14
|
+
|
|
15
|
+
from nimporter_plus.lib import (
|
|
16
|
+
WINDOWS, MACOS, LINUX, EXT_DIR, PLATFORM_TABLE, ARCH_TABLE, compiler_args, get_host_info
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
import nimporter_plus.nimporter # Register importers
|
|
20
|
+
|
|
21
|
+
from nimporter_plus.nexporter import get_nim_extensions, build_nim_extensions
|
nimporter_plus/cli.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
import shutil
|
|
7
|
+
import pathlib
|
|
8
|
+
import argparse
|
|
9
|
+
import subprocess
|
|
10
|
+
from typing import Any
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from cookiecutter.main import cookiecutter
|
|
13
|
+
from nimporter_plus.lib import *
|
|
14
|
+
from nimporter_plus.nimporter import *
|
|
15
|
+
|
|
16
|
+
# TODO(pbz): Need to move this to a doc/tutorial
|
|
17
|
+
SETUPPY_TEMPLATE: str = f'''
|
|
18
|
+
# Setup.py tutorial:
|
|
19
|
+
# https://github.com/navdeep-G/setup.py
|
|
20
|
+
# Edit `packages=` to fit your requirements
|
|
21
|
+
|
|
22
|
+
import setuptools, pathlib, sysconfig
|
|
23
|
+
from setuptools.command.build_ext import build_ext
|
|
24
|
+
import nimporter_plus
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class NoSuffixBuilder(build_ext):
|
|
28
|
+
"""
|
|
29
|
+
Optional.
|
|
30
|
+
|
|
31
|
+
Removes the target platform, architecture, and Python version from the
|
|
32
|
+
final artifact.
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
The artifact: `module.linux-x86_64.cpython.3.8.5.so`
|
|
36
|
+
Becomes: `module.so`
|
|
37
|
+
"""
|
|
38
|
+
def get_ext_filename(self, ext_name: str) -> str:
|
|
39
|
+
filename = super().get_ext_filename(ext_name)
|
|
40
|
+
ext_suffix = sysconfig.get_config_var('EXT_SUFFIX')
|
|
41
|
+
return filename.replace(ext_suffix, '') + pathlib.Path(filename).suffix
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
setuptools.setup(
|
|
45
|
+
name="{ pathlib.Path().absolute().name }",
|
|
46
|
+
packages=[..], # Please read the above tutorial
|
|
47
|
+
ext_modules=nimporter_plus.build_nim_extensions()
|
|
48
|
+
cmdclass={{"build_ext": NoSuffixBuilder}},
|
|
49
|
+
)
|
|
50
|
+
'''
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def nimporter_list() -> None:
|
|
54
|
+
for extension in find_extensions(Path()):
|
|
55
|
+
print(extension)
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def nimporter_clean(path: Path) -> None:
|
|
60
|
+
for item in path.iterdir():
|
|
61
|
+
item_full_path = item.resolve().absolute()
|
|
62
|
+
|
|
63
|
+
if item.is_dir():
|
|
64
|
+
DELETE_THESE_DIRS = {EXT_DIR, '__pycache__', '.pytest_cache'}
|
|
65
|
+
|
|
66
|
+
if item.stem in DELETE_THESE_DIRS:
|
|
67
|
+
print('Deleting', item_full_path)
|
|
68
|
+
shutil.rmtree(item)
|
|
69
|
+
|
|
70
|
+
elif item.name.endswith('.egg-info'):
|
|
71
|
+
print('Deleting', item_full_path)
|
|
72
|
+
shutil.rmtree(item)
|
|
73
|
+
|
|
74
|
+
elif item.stem == 'dist' and (item.parent / 'setup.py').exists():
|
|
75
|
+
print('Deleting', item_full_path)
|
|
76
|
+
shutil.rmtree(item)
|
|
77
|
+
|
|
78
|
+
elif item.stem == 'build' and (item.parent / 'setup.py').exists():
|
|
79
|
+
print('Deleting', item_full_path)
|
|
80
|
+
shutil.rmtree(item)
|
|
81
|
+
|
|
82
|
+
else:
|
|
83
|
+
nimporter_clean(item)
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def nimporter_init(extension_type: str, extension_name: str) -> None:
|
|
88
|
+
print(f'Initializing new extension {extension_type} "{extension_name}"')
|
|
89
|
+
|
|
90
|
+
if extension_type == 'mod':
|
|
91
|
+
Path(f'{extension_name}.nim').write_text(
|
|
92
|
+
'import nimpy\n\nproc add(a: int, b: int): int {.exportpy.} =\n'
|
|
93
|
+
' return a + b\n'
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
elif extension_type == 'lib':
|
|
97
|
+
cookiecutter(
|
|
98
|
+
template='https://github.com/Pebaz/template-nimporter-ext-lib',
|
|
99
|
+
extra_context=dict(ext_name=extension_name),
|
|
100
|
+
no_input=True
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
else:
|
|
104
|
+
raise ValueError(
|
|
105
|
+
f'Extension is not one of [`mod`, `lib`], got: {extension_type}'
|
|
106
|
+
)
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def nimporter_compile() -> None:
|
|
111
|
+
def current_time_ms() -> float:
|
|
112
|
+
return round(time.time() * 1000)
|
|
113
|
+
|
|
114
|
+
overall_start = current_time_ms()
|
|
115
|
+
|
|
116
|
+
nimporter_clean(Path())
|
|
117
|
+
|
|
118
|
+
for ext in find_extensions(Path()):
|
|
119
|
+
print(
|
|
120
|
+
f'Building Extension {"Lib" if ext.is_dir() else "Mod"}: '
|
|
121
|
+
f'{ext.name}'
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
start = current_time_ms()
|
|
125
|
+
module_path = ext if ext.is_file() else ext / f'{ext.name}.nim'
|
|
126
|
+
compile_extension_to_lib(ExtLib(module_path, Path(), ext.is_dir()))
|
|
127
|
+
print(' Completed in', current_time_ms() - start, 'ms')
|
|
128
|
+
|
|
129
|
+
print(
|
|
130
|
+
'Completed all in',
|
|
131
|
+
(current_time_ms() - overall_start) / 1000.0,
|
|
132
|
+
'seconds'
|
|
133
|
+
)
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
138
|
+
parser = argparse.ArgumentParser(description='Nimporter CLI')
|
|
139
|
+
subs = parser.add_subparsers(dest='cmd', required=True)
|
|
140
|
+
|
|
141
|
+
# List command
|
|
142
|
+
subs.add_parser('list', help='List Nim extensions starting in current dir')
|
|
143
|
+
|
|
144
|
+
# Clean command
|
|
145
|
+
subs.add_parser(
|
|
146
|
+
'clean',
|
|
147
|
+
help=(
|
|
148
|
+
'Run in project root to recursively remove __pycache__, '
|
|
149
|
+
'.egg-info, build, and dist folders used by Nimporter'
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Init command
|
|
154
|
+
init = subs.add_parser(
|
|
155
|
+
'init',
|
|
156
|
+
help='Initializes the folder structure of a new extension'
|
|
157
|
+
)
|
|
158
|
+
init.add_argument(
|
|
159
|
+
'extension_type',
|
|
160
|
+
type=str,
|
|
161
|
+
help=(
|
|
162
|
+
'Either `mod` or `lib`. Extension modules are single files, '
|
|
163
|
+
'extension libraries are are fully configurable mini Nim projects'
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
init.add_argument(
|
|
167
|
+
'extension_name',
|
|
168
|
+
type=str,
|
|
169
|
+
help='The importable name of the extension module or library'
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Compile command
|
|
173
|
+
subs.add_parser(
|
|
174
|
+
'compile',
|
|
175
|
+
help='Precompile all extensions exactly as if they were imported'
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return parser
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def main(cli_args: list[str] | None = None) -> int:
|
|
182
|
+
args = build_parser().parse_args(cli_args or sys.argv[1:])
|
|
183
|
+
|
|
184
|
+
if args.cmd == 'list':
|
|
185
|
+
nimporter_list()
|
|
186
|
+
|
|
187
|
+
elif args.cmd == 'clean':
|
|
188
|
+
# cwd = pathlib.Path()
|
|
189
|
+
# print('Cleaning Directory:', cwd.resolve())
|
|
190
|
+
# clean(cwd)
|
|
191
|
+
nimporter_clean(Path().resolve().absolute())
|
|
192
|
+
|
|
193
|
+
# elif args.cmd == 'bundle': # nimporter_bundle has nit been defined yet.
|
|
194
|
+
# nimporter_bundle(args.exp)
|
|
195
|
+
|
|
196
|
+
elif args.cmd == 'compile':
|
|
197
|
+
nimporter_compile()
|
|
198
|
+
|
|
199
|
+
elif args.cmd == 'init':
|
|
200
|
+
nimporter_init(args.extension_type, args.extension_name)
|
|
201
|
+
|
|
202
|
+
return 0
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
if __name__ == '__main__':
|
|
206
|
+
sys.exit(main(sys.argv[1:]))
|
nimporter_plus/lib.py
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import shlex
|
|
6
|
+
import shutil
|
|
7
|
+
import hashlib
|
|
8
|
+
import tempfile
|
|
9
|
+
import platform
|
|
10
|
+
import sysconfig
|
|
11
|
+
import subprocess
|
|
12
|
+
import cpuinfo
|
|
13
|
+
from typing import Any, Text
|
|
14
|
+
from collections.abc import Iterator
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from contextlib import contextmanager
|
|
17
|
+
from icecream import ic
|
|
18
|
+
|
|
19
|
+
PathParts = tuple[str, str, str] | tuple[str] | tuple[str, str]
|
|
20
|
+
|
|
21
|
+
PYTHON: str = 'python' if sys.platform == 'win32' else 'python3'
|
|
22
|
+
PIP: str = 'pip' if shutil.which('pip') else 'pip3'
|
|
23
|
+
PYTHON_LIB_EXT: str = sysconfig.get_config_var('EXT_SUFFIX')
|
|
24
|
+
WINDOWS: str = 'windows'
|
|
25
|
+
MACOS: str = 'darwin'
|
|
26
|
+
LINUX: str = 'linux'
|
|
27
|
+
EXT_DIR: str = 'nim-extensions'
|
|
28
|
+
|
|
29
|
+
PLATFORM_TABLE: dict[str, str] = { # Keys are known to Python and values are Nim-understood
|
|
30
|
+
'windows': 'Windows',
|
|
31
|
+
'darwin': 'MacOSX',
|
|
32
|
+
'linux': 'Linux',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
ARCH_TABLE: dict[str, str] = { # Keys are known to Python and values are Nim-understood
|
|
36
|
+
'x86_32': 'i386',
|
|
37
|
+
'x86_64': 'amd64',
|
|
38
|
+
'arm_8': 'arm64',
|
|
39
|
+
'aarch64': 'arm64',
|
|
40
|
+
'arm64': 'arm64',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
compiler_args: list[str] = [
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
ALWAYS_ARGS: list[str] = [
|
|
47
|
+
'nimble', # Installs dependencies :)
|
|
48
|
+
'c',
|
|
49
|
+
'--accept', # Allow installing dependencies
|
|
50
|
+
'--skipUserCfg',
|
|
51
|
+
'--app:lib',
|
|
52
|
+
'--backend:c',
|
|
53
|
+
'--threads:on',
|
|
54
|
+
'--warning[ProveInit]:off', # https://github.com/Pebaz/nimporter/issues/41
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def find_extensions(path: Path) -> list[Path]:
|
|
59
|
+
nim_exts = []
|
|
60
|
+
|
|
61
|
+
for item in path.iterdir():
|
|
62
|
+
if item.is_dir() and list(item.glob('*.nimble')):
|
|
63
|
+
# Treat directory as one single Extension
|
|
64
|
+
(nimble_file,) = item.glob('*.nimble')
|
|
65
|
+
|
|
66
|
+
# NOTE(pbz): Folder must contain .nimble file of exact same name
|
|
67
|
+
nim_file = nimble_file.parent / (nimble_file.stem + '.nim')
|
|
68
|
+
|
|
69
|
+
# NOTE(pbz): Folder must contain Nim file of exact same name.
|
|
70
|
+
if nim_file.exists():
|
|
71
|
+
nim_exts.append(item)
|
|
72
|
+
|
|
73
|
+
elif item.is_dir():
|
|
74
|
+
# Treat item as directory
|
|
75
|
+
nim_exts.extend(find_extensions(item))
|
|
76
|
+
|
|
77
|
+
elif item.suffix == '.nim':
|
|
78
|
+
# Treat item as a Nim Extension.
|
|
79
|
+
nim_exts.append(item)
|
|
80
|
+
|
|
81
|
+
return nim_exts
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_import_path(path: Path, root: Path) -> str:
|
|
85
|
+
"""
|
|
86
|
+
Coerce proper import path using root path
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
library(bool): hint as to how to treat the `module_path` parameter.
|
|
90
|
+
module_name(str): name of nim module.
|
|
91
|
+
module_path(Path): path to nim module.
|
|
92
|
+
root(Path): path to project root.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
str: Returns the path of the nim module.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def get_import_prefix(module_path: Path, root: Path) -> PathParts:
|
|
99
|
+
root_path = root.resolve()
|
|
100
|
+
full_path = module_path.resolve()
|
|
101
|
+
|
|
102
|
+
assert full_path >= root_path, 'Extension path is not within root dir.'
|
|
103
|
+
|
|
104
|
+
return ic(full_path.parts[len(root_path.parts):])
|
|
105
|
+
|
|
106
|
+
library = path.is_dir()
|
|
107
|
+
module_name = path.stem
|
|
108
|
+
module_path = path if path.is_file() else path / path.stem
|
|
109
|
+
import_prefix = get_import_prefix(module_path.parent, root)
|
|
110
|
+
module_part = tuple() if library else (module_name,)
|
|
111
|
+
import_path = '.'.join(import_prefix + module_part)
|
|
112
|
+
|
|
113
|
+
return ic(import_path)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@contextmanager
|
|
117
|
+
def convert_to_lib_if_needed(path: Path) -> Iterator[Path]:
|
|
118
|
+
if path.is_file():
|
|
119
|
+
package_root = None
|
|
120
|
+
for parent in path.parents:
|
|
121
|
+
nimbles = list(parent.glob('*.nimble'))
|
|
122
|
+
matching = [n for n in nimbles if n.stem in (path.stem, parent.name)]
|
|
123
|
+
if matching:
|
|
124
|
+
package_root = parent
|
|
125
|
+
break
|
|
126
|
+
|
|
127
|
+
if package_root:
|
|
128
|
+
yield package_root
|
|
129
|
+
else:
|
|
130
|
+
with tempfile.TemporaryDirectory() as compilation_dir:
|
|
131
|
+
shutil.copy(path, compilation_dir)
|
|
132
|
+
fake_nimble = Path(compilation_dir) / path.with_suffix('.nimble').name
|
|
133
|
+
fake_nimble.write_text('requires "nimpy"\n')
|
|
134
|
+
yield Path(compilation_dir)
|
|
135
|
+
else:
|
|
136
|
+
yield path
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def run_process(
|
|
140
|
+
process_args: list[str],
|
|
141
|
+
show_output: bool = False,
|
|
142
|
+
) -> tuple[int, bytes | Text, bytes | Text]:
|
|
143
|
+
"""
|
|
144
|
+
Invokes the compiler (or any executable) and returns the output.
|
|
145
|
+
|
|
146
|
+
While this can (and has been) used to call executables other than Nim
|
|
147
|
+
and Nimble, it should be noted that the warnings and hints are artifacts
|
|
148
|
+
of being mainly targeted as a Nim compiler invoker.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
process_args(list): the arg being the executable and the rest are args.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
A tuple containing any errors, warnings, or hints from the
|
|
155
|
+
compilation process.
|
|
156
|
+
"""
|
|
157
|
+
try:
|
|
158
|
+
process = subprocess.run(
|
|
159
|
+
process_args,
|
|
160
|
+
stdout=None if show_output else subprocess.PIPE,
|
|
161
|
+
stderr=None if show_output else subprocess.PIPE,
|
|
162
|
+
)
|
|
163
|
+
except FileNotFoundError:
|
|
164
|
+
executable = process_args[0] if process_args else "Command"
|
|
165
|
+
raise FileNotFoundError(
|
|
166
|
+
f"Cannot find '{executable}'. Please ensure it is installed and in your system PATH."
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
code, out, err = process.returncode, process.stdout, process.stderr
|
|
170
|
+
out = out.decode(errors='ignore') if out else '' # type: ignore[assignment]
|
|
171
|
+
err = err.decode(errors='ignore') if err else '' # type: ignore[assignment]
|
|
172
|
+
|
|
173
|
+
return code, out, err
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@contextmanager
|
|
177
|
+
def cd(path: Path) -> Iterator[Path]:
|
|
178
|
+
"Convenience function to step in and out of a directory temporarily."
|
|
179
|
+
cwd = os.getcwd()
|
|
180
|
+
os.chdir(path)
|
|
181
|
+
try:
|
|
182
|
+
yield path
|
|
183
|
+
finally:
|
|
184
|
+
os.chdir(cwd)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def get_c_compiler_used_to_build_python() -> str:
|
|
188
|
+
"This func is included just to be a bit clearer as to its significance."
|
|
189
|
+
return 'vcc' if 'MSC' in sys.version else 'gcc'
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def get_host_info() -> tuple[str, str, str]:
|
|
193
|
+
"""
|
|
194
|
+
Returns the host platform, architecture, and C compiler used to build the
|
|
195
|
+
running Python process.
|
|
196
|
+
"""
|
|
197
|
+
# Calling get_cpu_info() is expensive
|
|
198
|
+
if not getattr(get_host_info, 'host_arch', None):
|
|
199
|
+
setattr(get_host_info, 'host_arch', cpuinfo.get_cpu_info()['arch'].lower())
|
|
200
|
+
# get_host_info.host_arch = cpuinfo.get_cpu_info()['arch'].lower()
|
|
201
|
+
|
|
202
|
+
return ic((
|
|
203
|
+
platform.system().lower(),
|
|
204
|
+
get_host_info.host_arch, # type: ignore[attr-defined]
|
|
205
|
+
get_c_compiler_used_to_build_python()
|
|
206
|
+
))
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def ensure_nimpy() -> None:
|
|
210
|
+
"""
|
|
211
|
+
Makes sure that the Nimpy Nim library is installed.
|
|
212
|
+
|
|
213
|
+
Verifies that the [Nimpy Library](https://github.com/yglukhov/nimpy) is
|
|
214
|
+
installed and installs it otherwise.
|
|
215
|
+
|
|
216
|
+
NOTE: Nimporter would not be possible without Nimpy. Thank you
|
|
217
|
+
Yuriy Glukhov for making this project possible!
|
|
218
|
+
"""
|
|
219
|
+
ic()
|
|
220
|
+
|
|
221
|
+
show_output = 'NIMPORTER_INSTRUMENT' in os.environ
|
|
222
|
+
code, *_ = run_process(shlex.split('nimble path nimpy'), show_output)
|
|
223
|
+
|
|
224
|
+
if code != 0:
|
|
225
|
+
ic()
|
|
226
|
+
nimble_args = shlex.split('nimble install nimpy --accept')
|
|
227
|
+
code, _, stderr = run_process(nimble_args, show_output)
|
|
228
|
+
|
|
229
|
+
if code:
|
|
230
|
+
raise CompilationFailedException(stderr)
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class NimporterException(Exception):
|
|
235
|
+
"Base exception for Nimporter's exception hierarchy."
|
|
236
|
+
pass
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class CompilationFailedException(NimporterException):
|
|
240
|
+
def __init__(self, stderr: bytes | str) -> None:
|
|
241
|
+
if isinstance(stderr, bytes):
|
|
242
|
+
stderr_str = stderr.decode(errors='ignore')
|
|
243
|
+
else:
|
|
244
|
+
stderr_str = str(stderr)
|
|
245
|
+
super().__init__(
|
|
246
|
+
f'\n{"="*60}\nNim Compilation Failed:\n{stderr_str.strip()}\n{"="*60}'
|
|
247
|
+
)
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class ImportFailedException(NimporterException):
|
|
252
|
+
"Custom exception for when compilation succeeds but importing fails."
|
|
253
|
+
pass
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def hash_extension(module_path: Path) -> bytes:
|
|
257
|
+
"""
|
|
258
|
+
Convenience function to hash an extension module or extension library recursively,
|
|
259
|
+
including any associated project configuration files (.nimble, config.nims, .cfg).
|
|
260
|
+
"""
|
|
261
|
+
digest = hashlib.sha256()
|
|
262
|
+
root_folder = module_path.parent if module_path.is_file() else module_path
|
|
263
|
+
|
|
264
|
+
def walk_folder(path: Path) -> Iterator[Path]:
|
|
265
|
+
for item in sorted(path.iterdir()):
|
|
266
|
+
if item.is_dir():
|
|
267
|
+
if item.name in ('__pycache__', 'nimcache', '.git', '.venv', 'build', 'dist', '.pytest_cache', 'node_modules'):
|
|
268
|
+
continue
|
|
269
|
+
yield from walk_folder(item)
|
|
270
|
+
elif item.suffix in ('.nim', '.nimble', '.nims', '.cfg', '.json', '.c', '.h'):
|
|
271
|
+
yield item
|
|
272
|
+
|
|
273
|
+
for item in walk_folder(root_folder):
|
|
274
|
+
digest.update(str(item.relative_to(root_folder)).encode())
|
|
275
|
+
with item.open('rb') as f:
|
|
276
|
+
while buf := f.read(65536):
|
|
277
|
+
digest.update(buf)
|
|
278
|
+
|
|
279
|
+
# Also check parent directories up to project root for .nimble or config.nims
|
|
280
|
+
for parent in root_folder.parents:
|
|
281
|
+
if parent.name in ('dl', 'Users', 'home', ''):
|
|
282
|
+
break
|
|
283
|
+
configs = sorted([
|
|
284
|
+
c for c in parent.iterdir()
|
|
285
|
+
if c.is_file() and c.suffix in ('.nimble', '.nims', '.cfg')
|
|
286
|
+
])
|
|
287
|
+
for cfg in configs:
|
|
288
|
+
digest.update(str(cfg).encode())
|
|
289
|
+
with cfg.open('rb') as f:
|
|
290
|
+
while buf := f.read(65536):
|
|
291
|
+
digest.update(buf)
|
|
292
|
+
|
|
293
|
+
ic(digest.hexdigest())
|
|
294
|
+
return digest.digest()
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class ExtLib:
|
|
298
|
+
"""
|
|
299
|
+
All extensions are assumed to be libraries only. Modules are convert to
|
|
300
|
+
libraries as needed.
|
|
301
|
+
"""
|
|
302
|
+
def __init__(self, path: Path, root: Path, library_hint: bool) -> None:
|
|
303
|
+
"""
|
|
304
|
+
Args:
|
|
305
|
+
path(str): the relative path to the Nim file (for both lib & mod).
|
|
306
|
+
"""
|
|
307
|
+
self.library = all((
|
|
308
|
+
any(path.parent.glob(f'{path.stem}.nim')),
|
|
309
|
+
any(path.parent.glob(f'{path.stem}.nimble')),
|
|
310
|
+
any(path.parent.glob(f'{path.stem}.nim.cfg'))
|
|
311
|
+
))
|
|
312
|
+
|
|
313
|
+
assert library_hint and self.library if library_hint else True, (
|
|
314
|
+
f'ExtLib must define: {path.stem}.nim, {path.stem}.nimble, and '
|
|
315
|
+
f'{path.stem}.nim.cfg'
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
self.symbol = path.stem
|
|
319
|
+
self.module_path = path
|
|
320
|
+
|
|
321
|
+
if self.library:
|
|
322
|
+
self.relative_path = path.parent
|
|
323
|
+
self.full_path = path.parent.resolve().absolute()
|
|
324
|
+
|
|
325
|
+
else:
|
|
326
|
+
self.relative_path = path
|
|
327
|
+
self.full_path = path.resolve().absolute()
|
|
328
|
+
|
|
329
|
+
self.pycache = self.full_path.parent / '__pycache__'
|
|
330
|
+
|
|
331
|
+
self.import_namespace = get_import_path(self.relative_path, root)
|
|
332
|
+
self.hash_filename = self.pycache / f'{self.symbol}.hash'
|
|
333
|
+
self.build_artifact = (
|
|
334
|
+
self.pycache / f'{self.symbol}{PYTHON_LIB_EXT}'
|
|
335
|
+
)
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
def __str__(self) -> str:
|
|
339
|
+
return f'<ExtLib {self.import_namespace}>'
|
|
340
|
+
|
|
341
|
+
def __repr__(self) -> str:
|
|
342
|
+
return str(self)
|
|
343
|
+
|
|
344
|
+
def __format__(self, *args, **kwargs) -> str: # type: ignore[no-untyped-def]
|
|
345
|
+
return str(self)
|