wasm-action 0.0.8__py3-none-any.whl → 0.0.9__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.
- wasm_action/cache.py +58 -0
- wasm_action/cli.py +11 -0
- wasm_action/python.py +108 -0
- wasm_action/wasm/runtime.py +51 -2
- {wasm_action-0.0.8.dist-info → wasm_action-0.0.9.dist-info}/METADATA +2 -1
- {wasm_action-0.0.8.dist-info → wasm_action-0.0.9.dist-info}/RECORD +8 -6
- {wasm_action-0.0.8.dist-info → wasm_action-0.0.9.dist-info}/WHEEL +1 -1
- {wasm_action-0.0.8.dist-info → wasm_action-0.0.9.dist-info}/entry_points.txt +0 -0
wasm_action/cache.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
import hashlib
|
|
4
|
+
|
|
5
|
+
CACHE = os.path.join(os.environ['HOME'], '.cache', 'wasm-action')
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def fetch(content_hash: str) -> bytes:
|
|
9
|
+
"""Fetch object from cache"""
|
|
10
|
+
if not os.path.exists(CACHE):
|
|
11
|
+
os.makedirs(CACHE)
|
|
12
|
+
|
|
13
|
+
filename = os.path.join(CACHE, content_hash)
|
|
14
|
+
if not os.path.exists(filename):
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
with open(filename, 'rb') as f:
|
|
18
|
+
content = f.read()
|
|
19
|
+
|
|
20
|
+
computed_hash = hashlib.sha256(content).hexdigest()
|
|
21
|
+
if computed_hash != content_hash:
|
|
22
|
+
raise ValueError('content with hash has been tempered: {}'.format(content_hash))
|
|
23
|
+
return content
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def store(content: bytes):
|
|
27
|
+
"""Store object in cache"""
|
|
28
|
+
if not os.path.exists(CACHE):
|
|
29
|
+
os.makedirs(CACHE)
|
|
30
|
+
|
|
31
|
+
content_hash = hashlib.sha256(content).hexdigest()
|
|
32
|
+
filename = os.path.join(CACHE, content_hash)
|
|
33
|
+
with open(filename, 'wb') as f:
|
|
34
|
+
f.write(content)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def exists(content_hash: str) -> bool:
|
|
38
|
+
"""Check if object exists"""
|
|
39
|
+
if not os.path.exists(CACHE):
|
|
40
|
+
os.makedirs(CACHE)
|
|
41
|
+
|
|
42
|
+
filename = os.path.join(CACHE, content_hash)
|
|
43
|
+
return os.path.exists(filename)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def compute_hash(content: bytes) -> str:
|
|
47
|
+
"""Compute hash without storing the object"""
|
|
48
|
+
return hashlib.sha256(content).hexdigest()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def size(content_hash: str) -> int:
|
|
52
|
+
"""Return size of stored object"""
|
|
53
|
+
if not os.path.exists(CACHE):
|
|
54
|
+
os.makedirs(CACHE)
|
|
55
|
+
filename = os.path.join(CACHE, content_hash)
|
|
56
|
+
if not os.path.exists(filename):
|
|
57
|
+
return 0
|
|
58
|
+
return os.stat(filename).st_size
|
wasm_action/cli.py
CHANGED
|
@@ -5,6 +5,7 @@ import importlib.metadata
|
|
|
5
5
|
import json
|
|
6
6
|
|
|
7
7
|
from . import lib
|
|
8
|
+
from . import python
|
|
8
9
|
from .warg.crypto import generate_key
|
|
9
10
|
from .wasm import runtime
|
|
10
11
|
from .util import cli_error_handler
|
|
@@ -141,5 +142,15 @@ def evaluate(filename, expression):
|
|
|
141
142
|
print(e)
|
|
142
143
|
|
|
143
144
|
|
|
145
|
+
@cli.command('python', help="Python in a sandbox", add_help_option=False, context_settings=dict(
|
|
146
|
+
ignore_unknown_options=True,
|
|
147
|
+
))
|
|
148
|
+
@click.argument('args', nargs=-1, type=click.UNPROCESSED)
|
|
149
|
+
@cli_error_handler
|
|
150
|
+
def run_python(args):
|
|
151
|
+
"""Run a WASI-compiled wasm build of cpython"""
|
|
152
|
+
python.run_python(args)
|
|
153
|
+
|
|
154
|
+
|
|
144
155
|
if __name__ == "__main__":
|
|
145
156
|
cli()
|
wasm_action/python.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
|
|
2
|
+
"""
|
|
3
|
+
Python in a sandbox using WASM/WASI.
|
|
4
|
+
|
|
5
|
+
uvx --python 3.14 wasm-action python
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
import shutil
|
|
12
|
+
|
|
13
|
+
from . import cache
|
|
14
|
+
from . import lib
|
|
15
|
+
from .wasm import runtime
|
|
16
|
+
|
|
17
|
+
PYTHON = {
|
|
18
|
+
|
|
19
|
+
'3.14': {
|
|
20
|
+
'registry': 'wa.dev',
|
|
21
|
+
'package': 'xelato:python314',
|
|
22
|
+
'version': '26.2.6',
|
|
23
|
+
'sha256': '6a9e23d3db2ea0883fb74bfe9540bcb27bd167be1dab39546a5376112f4beea0',
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def run_python(args):
|
|
30
|
+
v = sys.version_info
|
|
31
|
+
version = "{}.{}".format(v.major, v.minor)
|
|
32
|
+
python = PYTHON.get(version) or PYTHON['3.14']
|
|
33
|
+
|
|
34
|
+
if cache.exists(python['sha256']):
|
|
35
|
+
print("Found object in cache")
|
|
36
|
+
content = cache.fetch(python['sha256'])
|
|
37
|
+
else:
|
|
38
|
+
print("Downloading python build")
|
|
39
|
+
download = lib.pull(
|
|
40
|
+
registry=python['registry'],
|
|
41
|
+
package="{}@{}".format(python['package'], python['version'])
|
|
42
|
+
)
|
|
43
|
+
if download.digest != "sha256:{}".format(python['sha256']):
|
|
44
|
+
raise ValueError('unexpected digest while downloading build')
|
|
45
|
+
|
|
46
|
+
cache.store(download.content)
|
|
47
|
+
print("Stored object in local cache")
|
|
48
|
+
content = download.content
|
|
49
|
+
|
|
50
|
+
# Lib folder
|
|
51
|
+
# Reusing the host Python installation
|
|
52
|
+
python_lib = os.path.dirname(os.__file__)
|
|
53
|
+
print("Using python lib {}".format(python_lib))
|
|
54
|
+
|
|
55
|
+
argv = ['python']
|
|
56
|
+
argv.extend(args)
|
|
57
|
+
|
|
58
|
+
tmp = tempfile.mkdtemp('py')
|
|
59
|
+
|
|
60
|
+
instance = (runtime
|
|
61
|
+
.module(content)
|
|
62
|
+
.wasi()
|
|
63
|
+
# pass all cli arguments to the wasm "process"
|
|
64
|
+
.argv(argv)
|
|
65
|
+
|
|
66
|
+
# configure python lib
|
|
67
|
+
.env('PYTHONPATH', '/lib:/build')
|
|
68
|
+
.mount(python_lib, '/lib', readonly=True)
|
|
69
|
+
|
|
70
|
+
# todo: ModuleNotFoundError: No module named '_sysconfigdata__wasi_wasm32-wasi'
|
|
71
|
+
#.mount('{}/github/python/cpython/builddir/wasi/build/lib.wasi-wasm32-3.14'.format(os.environ['HOME']), '/build')
|
|
72
|
+
|
|
73
|
+
# provide a /tmp folder
|
|
74
|
+
.mount(tmp, '/tmp', readonly=False)
|
|
75
|
+
|
|
76
|
+
# Get access to CWD for running user code.
|
|
77
|
+
# note: it may be preferable to set this to a subdir of /
|
|
78
|
+
.mount(os.getcwd(), "/", readonly=True)
|
|
79
|
+
|
|
80
|
+
.instance()
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# todo: exit code?
|
|
84
|
+
try:
|
|
85
|
+
instance.function('_start')()
|
|
86
|
+
finally:
|
|
87
|
+
# clean-up tmp dir
|
|
88
|
+
shutil.rmtree(tmp)
|
|
89
|
+
|
|
90
|
+
"""
|
|
91
|
+
https://github.com/python/cpython/blob/main/Modules/getpath.py
|
|
92
|
+
Could not find platform independent libraries <prefix>
|
|
93
|
+
Could not find platform dependent libraries <exec_prefix>
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
# 3.14
|
|
97
|
+
"""
|
|
98
|
+
Installations of Python now contain a new file, :file:`build-details.json`.
|
|
99
|
+
This is a static JSON document containing build details for CPython,
|
|
100
|
+
to allow for introspection without needing to run code.
|
|
101
|
+
This is helpful for use-cases such as Python launchers, cross-compilation,
|
|
102
|
+
and so on.
|
|
103
|
+
|
|
104
|
+
:file:`build-details.json` must be installed in the platform-independent
|
|
105
|
+
standard library directory. This corresponds to the :ref:`'stdlib'
|
|
106
|
+
<installation_paths>` :mod:`sysconfig` installation path,
|
|
107
|
+
which can be found by running ``sysconfig.get_path('stdlib')``.
|
|
108
|
+
"""
|
wasm_action/wasm/runtime.py
CHANGED
|
@@ -24,16 +24,21 @@ class Module:
|
|
|
24
24
|
self._store = wasmtime.Store()
|
|
25
25
|
self._module = wasmtime.Module(self._store.engine, module_bytes)
|
|
26
26
|
|
|
27
|
+
def wasi(self):
|
|
28
|
+
return WASI(store=self._store, module=self._module)
|
|
29
|
+
|
|
27
30
|
def instance(self):
|
|
28
31
|
return Instance(store=self._store, module=self._module)
|
|
29
32
|
|
|
30
33
|
|
|
31
34
|
class Instance:
|
|
32
35
|
|
|
33
|
-
def __init__(self, store, module):
|
|
36
|
+
def __init__(self, store, module, instance=None):
|
|
34
37
|
self._store = store
|
|
35
38
|
self._module = module
|
|
36
|
-
|
|
39
|
+
if instance:
|
|
40
|
+
assert isinstance(instance, wasmtime.Instance)
|
|
41
|
+
self._instance = instance
|
|
37
42
|
self._imports = []
|
|
38
43
|
|
|
39
44
|
def __getattr__(self, name):
|
|
@@ -54,6 +59,50 @@ class Instance:
|
|
|
54
59
|
return expression.evaluate(text or '', obj=self)
|
|
55
60
|
|
|
56
61
|
|
|
62
|
+
class WASI:
|
|
63
|
+
|
|
64
|
+
def __init__(self, store, module):
|
|
65
|
+
self._store = store
|
|
66
|
+
self._module = module
|
|
67
|
+
|
|
68
|
+
self.linker = wasmtime.Linker(self._store.engine)
|
|
69
|
+
self.linker.define_wasi()
|
|
70
|
+
|
|
71
|
+
self._wasi = wasmtime.WasiConfig()
|
|
72
|
+
self._wasi.inherit_stdin()
|
|
73
|
+
self._wasi.inherit_stdout()
|
|
74
|
+
self._wasi.inherit_stderr()
|
|
75
|
+
|
|
76
|
+
self._environ = {}
|
|
77
|
+
|
|
78
|
+
def mount(self, host_path, guest_path, readonly=True):
|
|
79
|
+
"""Bind mount `host_path` as `guest_path` in the WASM instance."""
|
|
80
|
+
self._wasi.preopen_dir(
|
|
81
|
+
host_path,
|
|
82
|
+
guest_path,
|
|
83
|
+
dir_perms=wasmtime.DirPerms.READ_ONLY if readonly else wasmtime.DirPerms.READ_WRITE,
|
|
84
|
+
file_perms=wasmtime.FilePerms.READ_ONLY if readonly else wasmtime.FilePerms.READ_WRITE,
|
|
85
|
+
)
|
|
86
|
+
return self
|
|
87
|
+
|
|
88
|
+
def argv(self, argv):
|
|
89
|
+
self._wasi.argv = argv
|
|
90
|
+
return self
|
|
91
|
+
|
|
92
|
+
def env(self, key, value):
|
|
93
|
+
"""Configure an environment variable in the WASM instance"""
|
|
94
|
+
self._environ[key] = value
|
|
95
|
+
return self
|
|
96
|
+
|
|
97
|
+
def instance(self):
|
|
98
|
+
self._wasi.env = [
|
|
99
|
+
(k, v) for (k, v) in self._environ.items()
|
|
100
|
+
]
|
|
101
|
+
self._store.set_wasi(self._wasi)
|
|
102
|
+
instance = self.linker.instantiate(self._store, self._module)
|
|
103
|
+
return Instance(store=self._store, module=self._module, instance=instance)
|
|
104
|
+
|
|
105
|
+
|
|
57
106
|
class Function:
|
|
58
107
|
|
|
59
108
|
_types = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: wasm-action
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.9
|
|
4
4
|
Summary: Interact with WebAssembly registries.
|
|
5
5
|
Requires-Dist: click>=8.2.1
|
|
6
6
|
Requires-Dist: cryptography>=45.0.6
|
|
@@ -27,6 +27,7 @@ Description-Content-Type: text/markdown
|
|
|
27
27
|
* Supported artifact types: wasm
|
|
28
28
|
* Supported actions: push, pull
|
|
29
29
|
* Supports Python 3.10+ on Linux, MacOS and Windows
|
|
30
|
+
* Python sandbox using wasm build of cpython 3.14
|
|
30
31
|
|
|
31
32
|
#### Planned
|
|
32
33
|
* OCI registry support (a.k.a. Docker registry)
|
|
@@ -58,8 +58,10 @@ warg_openapi/models/timestamped_checkpoint.py,sha256=HvpqFUAzxb4AlhFOd1FHi8gKYAv
|
|
|
58
58
|
warg_openapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
59
59
|
warg_openapi/rest.py,sha256=esTaIvd-RN8XURlnK_ALzgEyd5n1853hJRoAdCe7C3k,14044
|
|
60
60
|
wasm_action/__init__.py,sha256=77Mz6_mfveQMO90YlCumicDwhzSa0Xvi3xpJ_10EYZ8,190
|
|
61
|
-
wasm_action/
|
|
61
|
+
wasm_action/cache.py,sha256=6t77ZzcZawZjilZZa7tRXY-beIJfZ1PJ7yTkxRezTzw,1530
|
|
62
|
+
wasm_action/cli.py,sha256=DaOdpSbf8_jJ_xehflzBHW7iR9Ma5sejnuwweIuPDrs,4498
|
|
62
63
|
wasm_action/lib.py,sha256=xX66hhbxyxI5GP_k4DfDdViWj0FonqFXBwq8ssAuZo4,5366
|
|
64
|
+
wasm_action/python.py,sha256=porsC0J_NK-FgBSeYFe1s-Znx6Q4SOv8G4tVQMbjPBI,3104
|
|
63
65
|
wasm_action/registry.py,sha256=JfsYJbzysbcgMAG9wf-8bPU-fmSm7wan_aReRXGGaMM,1497
|
|
64
66
|
wasm_action/util.py,sha256=yxklrEViwjMn4k3f6_ckkqWD1il9R0hJj6qBsP7CrF4,3600
|
|
65
67
|
wasm_action/warg/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -69,8 +71,8 @@ wasm_action/warg/crypto.py,sha256=eK2Vw1mLFg2wmL6_3jmLHKzo2JK3KpnOz4PRUFZowZY,49
|
|
|
69
71
|
wasm_action/warg/proto.py,sha256=ui6rPfC_5IQQheOAwCxOL--r_tmvN-hc50NlLC5bgn4,6226
|
|
70
72
|
wasm_action/wasm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
71
73
|
wasm_action/wasm/expression.py,sha256=0DWJbHKe50oIFkL11VTdkyCiIruQH7pXpG2SsI_Jr3g,2630
|
|
72
|
-
wasm_action/wasm/runtime.py,sha256=
|
|
73
|
-
wasm_action-0.0.
|
|
74
|
-
wasm_action-0.0.
|
|
75
|
-
wasm_action-0.0.
|
|
76
|
-
wasm_action-0.0.
|
|
74
|
+
wasm_action/wasm/runtime.py,sha256=PNX4tDJxaBMYuUkYts_htmSZvk889kTm7bCyWULiPdo,4249
|
|
75
|
+
wasm_action-0.0.9.dist-info/WHEEL,sha256=iHtWm8nRfs0VRdCYVXocAWFW8ppjHL-uTJkAdZJKOBM,80
|
|
76
|
+
wasm_action-0.0.9.dist-info/entry_points.txt,sha256=lTdloFNHGTiEbfd1efp0ZQ7p9SpAbzIWYqxk__EAPFE,53
|
|
77
|
+
wasm_action-0.0.9.dist-info/METADATA,sha256=Yz-7K4wWp-r8Zcvk1tKpzvkgox1jt-jtv-90bkSonwI,4971
|
|
78
|
+
wasm_action-0.0.9.dist-info/RECORD,,
|
|
File without changes
|