auditwheel-emscripten 0.0.15__tar.gz → 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.
Files changed (50) hide show
  1. auditwheel_emscripten-0.1.0/.github/workflows/deploy.yml +71 -0
  2. auditwheel_emscripten-0.1.0/.github/workflows/test.yml +33 -0
  3. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/.pre-commit-config.yaml +15 -9
  4. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/CHANGELOG.md +14 -0
  5. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/PKG-INFO +5 -5
  6. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/README.md +1 -1
  7. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/auditwheel_emscripten/__init__.py +2 -2
  8. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/auditwheel_emscripten/cli/main.py +1 -1
  9. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/auditwheel_emscripten/emscripten_tools/webassembly.py +10 -0
  10. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/auditwheel_emscripten/module.py +36 -5
  11. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/auditwheel_emscripten/repair.py +19 -11
  12. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/auditwheel_emscripten/show.py +1 -1
  13. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/auditwheel_emscripten/wheel_utils.py +22 -9
  14. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/pyproject.toml +2 -2
  15. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/test/test_repair.py +41 -1
  16. auditwheel_emscripten-0.1.0/uv.lock +200 -0
  17. auditwheel_emscripten-0.0.15/.github/workflows/deploy.yml +0 -50
  18. auditwheel_emscripten-0.0.15/.github/workflows/test.yml +0 -25
  19. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/.flake8 +0 -0
  20. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/.gitignore +0 -0
  21. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/LICENSE +0 -0
  22. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/auditwheel_emscripten/cli/__init__.py +0 -0
  23. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/auditwheel_emscripten/emscripten_tools/__init__.py +0 -0
  24. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/auditwheel_emscripten/emscripten_tools/diagnostics.py +0 -0
  25. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/auditwheel_emscripten/emscripten_tools/utils.py +0 -0
  26. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/auditwheel_emscripten/exports.py +0 -0
  27. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/auditwheel_emscripten/function_type.py +0 -0
  28. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/auditwheel_emscripten/imports.py +0 -0
  29. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/auditwheel_emscripten/lib_utils.py +0 -0
  30. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/auditwheel_emscripten/py.typed +0 -0
  31. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/auditwheel_emscripten/wasm_utils.py +0 -0
  32. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/test/paths.py +0 -0
  33. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/test/test_data/Shapely-1.8.2-cp310-cp310-emscripten_3_1_14_wasm32.whl +0 -0
  34. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/test/test_data/elf +0 -0
  35. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/test/test_data/galpy-1.8.0-cp310-cp310-emscripten_3_1_14_wasm32.whl +0 -0
  36. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/test/test_data/libcrypto.so +0 -0
  37. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/test/test_data/libgeos.so +0 -0
  38. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/test/test_data/libgeos.so.3.10.3 +0 -0
  39. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/test/test_data/libgeos_c.so +0 -0
  40. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/test/test_data/libgeos_c.so.1 +0 -0
  41. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/test/test_data/libgeos_c.so.1.16.1 +0 -0
  42. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/test/test_data/libgeos_c.wasm +0 -0
  43. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/test/test_data/libssl.so +0 -0
  44. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/test/test_data/numpy-1.22.4-cp310-cp310-emscripten_3_1_14_wasm32.whl +0 -0
  45. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/test/test_exports.py +0 -0
  46. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/test/test_function_type.py +0 -0
  47. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/test/test_imports.py +0 -0
  48. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/test/test_show.py +0 -0
  49. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/test/test_utils.py +0 -0
  50. {auditwheel_emscripten-0.0.15 → auditwheel_emscripten-0.1.0}/test/test_wheel_utils.py +0 -0
@@ -0,0 +1,71 @@
1
+ name: deploy
2
+
3
+ on:
4
+ release:
5
+ types:
6
+ - published
7
+ schedule:
8
+ - cron: "0 3 * * 1"
9
+
10
+ permissions: {}
11
+
12
+ concurrency:
13
+ group: main-${{ github.head_ref || github.run_id }}
14
+ cancel-in-progress: true
15
+
16
+ jobs:
17
+ build:
18
+ runs-on: ubuntu-latest
19
+ permissions:
20
+ contents: read
21
+ steps:
22
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
23
+ with:
24
+ persist-credentials: false
25
+
26
+ - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
27
+
28
+ - name: Install requirements and build wheel
29
+ shell: bash -l {0}
30
+ run: |
31
+ python -m pip install build
32
+ python -m build .
33
+
34
+ - name: Store the distribution packages
35
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
36
+ with:
37
+ name: python-package-distributions
38
+ path: dist/
39
+ if-no-files-found: error
40
+
41
+ publish-to-pypi:
42
+ name: Publish Python 🐍 distribution 📦 to PyPI
43
+ needs:
44
+ - build
45
+ runs-on: ubuntu-latest
46
+ if: github.event_name == 'release' && github.event.action == 'published'
47
+ environment:
48
+ name: pypi
49
+ url: https://pypi.org/p/auditwheel-emscripten
50
+ permissions:
51
+ contents: write
52
+ id-token: write # IMPORTANT: mandatory for trusted publishing
53
+ attestations: write
54
+ steps:
55
+ - name: Download all the dists
56
+ uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
57
+ with:
58
+ name: python-package-distributions
59
+ path: dist/
60
+ merge-multiple: true
61
+
62
+ - name: Generate artifact attestation(s)
63
+ uses: actions/attest-build-provenance@7668571508540a607bdfd90a87a560489fe372eb # v2.1.0
64
+ with:
65
+ subject-path: |
66
+ dist/*.tar.gz
67
+ dist/*.whl
68
+
69
+ - name: Publish distribution 📦 to PyPI
70
+ if: github.event_name == 'release' && github.event.action == 'published'
71
+ uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
@@ -0,0 +1,33 @@
1
+ name: test
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+
9
+ permissions: {}
10
+
11
+ concurrency:
12
+ group: main-${{ github.head_ref || github.run_id }}
13
+ cancel-in-progress: true
14
+
15
+ jobs:
16
+ test:
17
+ runs-on: ubuntu-latest
18
+ permissions:
19
+ contents: read
20
+ steps:
21
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
22
+ with:
23
+ persist-credentials: false
24
+ fetch-depth: 0
25
+
26
+ - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
27
+ with:
28
+ python-version: "3.12"
29
+ - name: Install dependencies
30
+ run: python -m pip install -e ".[test]"
31
+
32
+ - name: Run tests
33
+ run: pytest -svra
@@ -1,9 +1,9 @@
1
1
  exclude: (^auditwheel_emscripten/emscripten_tools/)
2
2
  default_language_version:
3
- python: "3.11"
3
+ python: "3.12"
4
4
  repos:
5
5
  - repo: https://github.com/pre-commit/pre-commit-hooks
6
- rev: "v4.6.0"
6
+ rev: "v5.0.0"
7
7
  hooks:
8
8
  - id: check-case-conflict
9
9
  - id: check-merge-conflict
@@ -16,31 +16,31 @@ repos:
16
16
  - id: trailing-whitespace
17
17
 
18
18
  - repo: https://github.com/asottile/pyupgrade
19
- rev: "v3.16.0"
19
+ rev: "v3.19.1"
20
20
  hooks:
21
21
  - id: pyupgrade
22
- args: ["--py310-plus"]
22
+ args: ["--py312-plus"]
23
23
 
24
24
  - repo: https://github.com/hadialqattan/pycln
25
- rev: "v2.4.0"
25
+ rev: "v2.5.0"
26
26
  hooks:
27
27
  - id: pycln
28
28
  args: [--config=pyproject.toml]
29
29
  stages: [manual]
30
30
 
31
31
  - repo: https://github.com/psf/black
32
- rev: "24.4.2"
32
+ rev: "25.1.0"
33
33
  hooks:
34
34
  - id: black
35
35
 
36
36
  - repo: https://github.com/pycqa/flake8
37
- rev: "7.0.0"
37
+ rev: "7.1.2"
38
38
  hooks:
39
39
  - id: flake8
40
40
  additional_dependencies: [flake8-bugbear]
41
41
 
42
42
  - repo: https://github.com/pre-commit/mirrors-mypy
43
- rev: "v1.10.0"
43
+ rev: "v1.15.0"
44
44
  hooks:
45
45
  - id: mypy
46
46
  exclude: auditwheel_emscripten/emscripten_tools
@@ -51,7 +51,13 @@ repos:
51
51
  - id: shellcheck
52
52
 
53
53
  - repo: https://github.com/codespell-project/codespell
54
- rev: "v2.3.0"
54
+ rev: "v2.4.1"
55
55
  hooks:
56
56
  - id: codespell
57
57
  args: ["-L", "te,slowy,aray,ba,nd,classs,crate,feld,lits"]
58
+
59
+ - repo: https://github.com/woodruffw/zizmor-pre-commit
60
+ rev: "v1.5.2"
61
+ hooks:
62
+ - id: zizmor
63
+ args: ["--pedantic"]
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## [0.1.0] - 2025-04-04
11
+
12
+ ### Changed
13
+
14
+ - `auditwheel repair` command now updates the DYLINK RPATH section of the wasm module.
15
+ ([#41](https://github.com/ryanking13/auditwheel-emscripten/pull/41))
16
+
17
+ ## [0.0.16] - 2024-07-05
18
+
19
+ ### Fixed
20
+
21
+ - Fixed incorrect extra dependency for typer
22
+ ([#32](https://github.com/ryanking13/auditwheel-emscripten/pull/32))
23
+
10
24
  ## [0.0.15] - 2024-06-17
11
25
 
12
26
  ### Fixed
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: auditwheel_emscripten
3
- Version: 0.0.15
3
+ Version: 0.1.0
4
4
  Summary: auditwheel-like tool for Pyodide
5
5
  Project-URL: Home, https://github.com/ryanking13/auditwheel-emscripten
6
6
  Author-email: Gyeongjae Choi <def6488@gmail.com>
@@ -378,11 +378,11 @@ License: Mozilla Public License Version 2.0
378
378
  This Source Code Form is "Incompatible With Secondary Licenses", as
379
379
  defined by the Mozilla Public License, v. 2.0.
380
380
  License-File: LICENSE
381
- Requires-Python: >=3.10
381
+ Requires-Python: >=3.12
382
382
  Requires-Dist: leb128
383
383
  Requires-Dist: packaging
384
384
  Requires-Dist: pyodide-cli
385
- Requires-Dist: typer[all]
385
+ Requires-Dist: typer
386
386
  Requires-Dist: wheel
387
387
  Provides-Extra: test
388
388
  Requires-Dist: pytest; extra == 'test'
@@ -407,7 +407,7 @@ auditwheel-emscripten is a tiny tool to facilitate the creation of Python wheel
407
407
  Python-in-the-browser using Emscripten.
408
408
 
409
409
  - `pyodide auditwheel show`: shows external shared libraries that the wheel depends on.
410
- - `pyodide auditwheel repair`: copies these external shared libraries into the wheel itself.
410
+ - `pyodide auditwheel repair`: copies these external shared libraries into the wheel itself, and update the RPATH of the WASM module correspondingly.
411
411
 
412
412
  ## Usage (CLI)
413
413
 
@@ -17,7 +17,7 @@ auditwheel-emscripten is a tiny tool to facilitate the creation of Python wheel
17
17
  Python-in-the-browser using Emscripten.
18
18
 
19
19
  - `pyodide auditwheel show`: shows external shared libraries that the wheel depends on.
20
- - `pyodide auditwheel repair`: copies these external shared libraries into the wheel itself.
20
+ - `pyodide auditwheel repair`: copies these external shared libraries into the wheel itself, and update the RPATH of the WASM module correspondingly.
21
21
 
22
22
  ## Usage (CLI)
23
23
 
@@ -1,12 +1,12 @@
1
1
  from .exports import get_exports
2
2
  from .imports import get_imports
3
- from .repair import copylib, repair, repair_extracted, resolve_sharedlib
3
+ from .repair import copylib, repair, resolve_sharedlib, modify_runtime_path
4
4
  from .show import show, show_dylib, show_wheel
5
5
 
6
6
  __all__ = [
7
7
  "repair",
8
- "repair_extracted",
9
8
  "resolve_sharedlib",
9
+ "modify_runtime_path",
10
10
  "show",
11
11
  "show_wheel",
12
12
  "show_dylib",
@@ -50,7 +50,7 @@ def _repair(
50
50
  wheel_file,
51
51
  libdir,
52
52
  output_dir,
53
- modify_needed_section=False,
53
+ modify_rpath=True,
54
54
  )
55
55
  dependencies = show(repaired_wheel)
56
56
  pprint(dependencies)
@@ -157,6 +157,7 @@ class DylinkType(IntEnum):
157
157
  NEEDED = 2
158
158
  EXPORT_INFO = 3
159
159
  IMPORT_INFO = 4
160
+ RUNTIME_PATH = 5
160
161
 
161
162
 
162
163
  class InvalidWasmError(BaseException):
@@ -178,6 +179,7 @@ Dylink = namedtuple(
178
179
  "needed",
179
180
  "export_info",
180
181
  "import_info",
182
+ "runtime_paths",
181
183
  ],
182
184
  )
183
185
  Table = namedtuple("Table", ["elem_type", "limits"])
@@ -332,6 +334,7 @@ class Module:
332
334
  needed = []
333
335
  export_info = {}
334
336
  import_info = {}
337
+ runtime_paths = []
335
338
  self.read_string() # name
336
339
 
337
340
  if dylink_section.name == "dylink":
@@ -378,6 +381,12 @@ class Module:
378
381
  import_info.setdefault(module, {})
379
382
  import_info[module][field] = flags
380
383
  count -= 1
384
+ elif subsection_type == DylinkType.RUNTIME_PATH:
385
+ count = self.read_uleb()
386
+ while count:
387
+ rpath = self.read_string()
388
+ runtime_paths.append(rpath)
389
+ count -= 1
381
390
  else:
382
391
  print(f"unknown subsection: {subsection_type}")
383
392
  # ignore unknown subsections
@@ -395,6 +404,7 @@ class Module:
395
404
  needed,
396
405
  export_info,
397
406
  import_info,
407
+ runtime_paths,
398
408
  )
399
409
 
400
410
  @memoize
@@ -76,6 +76,17 @@ class ModuleWritable(webassembly.Module):
76
76
  buf.extend(leb128.u.encode(len(subsection_buf)))
77
77
  buf.extend(subsection_buf)
78
78
 
79
+ # 5. RUNTIME_PATH
80
+ subsection_buf = bytearray()
81
+ subsection_buf.extend(leb128.u.encode(len(dylink.runtime_paths)))
82
+ for path in dylink.runtime_paths:
83
+ subsection_buf.extend(leb128.u.encode(len(path.encode())))
84
+ subsection_buf += path.encode()
85
+
86
+ buf.extend(leb128.u.encode(DylinkType.RUNTIME_PATH))
87
+ buf.extend(leb128.u.encode(len(subsection_buf)))
88
+ buf.extend(subsection_buf)
89
+
79
90
  section_buf = bytearray()
80
91
 
81
92
  # custom section
@@ -115,6 +126,29 @@ class ModuleWritable(webassembly.Module):
115
126
 
116
127
  return patched_module
117
128
 
129
+ def patch_runtime_path(self, runtime_path: Path) -> bytes:
130
+ curfile = Path(self.filename).resolve()
131
+
132
+ relpath = os.path.relpath(runtime_path, curfile.parent)
133
+ if relpath == ".":
134
+ realpath_with_prefix = "$ORIGIN"
135
+ else:
136
+ realpath_with_prefix = "$ORIGIN/" + Path(relpath).as_posix()
137
+
138
+ dylink_section: webassembly.Dylink = self.parse_dylink_section()
139
+ runtime_paths = dylink_section.runtime_paths
140
+
141
+ if realpath_with_prefix not in runtime_paths:
142
+ runtime_paths = runtime_paths + [realpath_with_prefix]
143
+
144
+ patched_dylink_section = dylink_section._replace(
145
+ runtime_paths=runtime_paths,
146
+ )
147
+ encoded_dylink_section = self.encode_dylink_section(patched_dylink_section)
148
+ patched_module = self.patch_dylink(encoded_dylink_section)
149
+
150
+ return patched_module
151
+
118
152
 
119
153
  def parse_dylink_section(dylib: Path):
120
154
  """
@@ -126,12 +160,9 @@ def parse_dylink_section(dylib: Path):
126
160
  return dylink
127
161
 
128
162
 
129
- def patch_needed_libs_path(dylib: Path, dep_map: dict[str, Path]):
130
- """
131
- Patch needed path in dylink section
132
- """
163
+ def patch_runtime_path(dylib: Path, runtime_path: Path):
133
164
  with ModuleWritable(dylib) as m:
134
- patched_module = m.patch_needed_path(dep_map)
165
+ patched_module = m.patch_runtime_path(runtime_path)
135
166
 
136
167
  return patched_module
137
168
 
@@ -4,7 +4,7 @@ from collections import deque
4
4
  from pathlib import Path
5
5
 
6
6
  from .lib_utils import get_all_shared_libs_in_dir, libdir_candidates
7
- from .module import patch_needed_libs_path
7
+ from .module import patch_runtime_path
8
8
  from .show import show
9
9
  from .wheel_utils import WHEEL_INFO_RE, is_emscripten_wheel, pack, unpack
10
10
 
@@ -60,27 +60,33 @@ def copylib(
60
60
  return new_dep_map
61
61
 
62
62
 
63
- def repair_extracted(
64
- wheel_extract_dir: str | Path,
65
- dep_map: dict[str, Path],
66
- lib_sdir: str,
67
- ) -> dict[str, Path]:
68
- dep_map_copied = copylib(wheel_extract_dir, dep_map, lib_sdir)
63
+ def modify_runtime_path(wheel_extract_dir: str | Path, runtime_path: str) -> None:
64
+ """
65
+ Patch the runtime path of shared libraries inside the wheel file
66
+
67
+ Parameters
68
+ ----------
69
+ wheel_extract_dir : str | Path
70
+ The directory containing the extracted wheel file
71
+
72
+ runtime_path : str
73
+ The target directory name where the shared libraries are located
74
+ """
75
+ runtime_path_full = Path(wheel_extract_dir) / runtime_path
76
+ assert runtime_path_full.exists(), f"lib directory not found: {runtime_path_full}"
69
77
 
70
78
  shared_libs = get_all_shared_libs_in_dir(wheel_extract_dir)
71
79
  for shared_lib in shared_libs:
72
- patched_module = patch_needed_libs_path(shared_lib, dep_map_copied)
80
+ patched_module = patch_runtime_path(shared_lib, runtime_path_full)
73
81
  shared_lib.write_bytes(patched_module)
74
82
 
75
- return dep_map_copied
76
-
77
83
 
78
84
  def repair(
79
85
  wheel_file: str | Path,
80
86
  libdir: str | Path,
81
87
  outdir: str | Path | None,
82
88
  lib_sdir: str = ".libs",
83
- modify_needed_section: bool = False, # Deprecated
89
+ modify_rpath: bool = True,
84
90
  ) -> Path:
85
91
  file = Path(wheel_file)
86
92
  if not file.exists():
@@ -101,6 +107,8 @@ def repair(
101
107
 
102
108
  extract_dir = unpack(str(wheel_file), str(tmpdir))
103
109
  copylib(extract_dir, dep_map, lib_sdir)
110
+ if modify_rpath:
111
+ modify_runtime_path(extract_dir, lib_sdir)
104
112
  pack(str(extract_dir), str(outdir), None)
105
113
 
106
114
  return outdir / file.name
@@ -21,7 +21,7 @@ def show_wheel_unpacked(wheel_extract_dir: str | Path) -> dict[str, list[str]]:
21
21
  shared_libs = get_all_shared_libs_in_dir(wheel_extract_dir)
22
22
  for shared_lib in shared_libs:
23
23
  deps = show_dylib(shared_lib)
24
- dependencies[str(shared_lib.relative_to(wheel_extract_dir))] = deps
24
+ dependencies[shared_lib.relative_to(wheel_extract_dir).as_posix()] = deps
25
25
 
26
26
  return dependencies
27
27
 
@@ -1,13 +1,11 @@
1
- import io
2
1
  import re
3
- from contextlib import redirect_stdout, contextmanager
4
- from pathlib import Path
2
+ import subprocess
5
3
  import tempfile
4
+ from contextlib import contextmanager
5
+ from pathlib import Path
6
6
  from collections.abc import Generator
7
7
 
8
8
  from packaging.utils import parse_wheel_filename
9
- from wheel.cli.pack import pack as pack_wheel
10
- from wheel.cli.unpack import unpack as unpack_wheel
11
9
 
12
10
  # Copied from auditwheel
13
11
  WHEEL_INFO_RE = re.compile(
@@ -61,8 +59,15 @@ def unpack(path: str | Path, dest: str | Path = ".") -> Path:
61
59
  path = str(path)
62
60
  dest = str(dest)
63
61
 
64
- with io.StringIO() as buf, redirect_stdout(buf):
65
- unpack_wheel(path, dest)
62
+ result = subprocess.run(
63
+ ["wheel", "unpack", path, "--dest", dest],
64
+ check=True,
65
+ encoding="utf-8",
66
+ capture_output=True,
67
+ )
68
+
69
+ if result.returncode != 0:
70
+ raise RuntimeError(f"Failed to unpack wheel: {result.stderr}")
66
71
 
67
72
  return Path(dest) / parse_wheel_extract_dir(path)
68
73
 
@@ -73,9 +78,17 @@ def pack(directory: str | Path, dest_dir: str | Path, build_number: str | None)
73
78
  wheel file name can be determined.
74
79
  :param directory: The unpacked wheel directory
75
80
  :param dest_dir: Destination directory (defaults to the current directory)
81
+ :param build_number: Optional build number for the wheel
76
82
  """
77
83
  directory = str(directory)
78
84
  dest_dir = str(dest_dir)
79
85
 
80
- with io.StringIO() as buf, redirect_stdout(buf):
81
- pack_wheel(directory, dest_dir, build_number)
86
+ cmd = ["wheel", "pack", directory, "--dest-dir", dest_dir]
87
+
88
+ if build_number is not None:
89
+ cmd.extend(["--build-number", build_number])
90
+
91
+ result = subprocess.run(cmd, check=True, encoding="utf-8", capture_output=True)
92
+
93
+ if result.returncode != 0:
94
+ raise RuntimeError(f"Failed to pack wheel: {result.stderr}")
@@ -8,12 +8,12 @@ authors = [{name = "Gyeongjae Choi", email = "def6488@gmail.com"}]
8
8
  readme = "README.md"
9
9
  description = "auditwheel-like tool for Pyodide"
10
10
  dynamic = ["version"]
11
- requires-python = ">=3.10"
11
+ requires-python = ">=3.12"
12
12
  dependencies = [
13
13
  "leb128",
14
14
  "packaging",
15
15
  "pyodide-cli",
16
- "typer[all]",
16
+ "typer",
17
17
  "wheel",
18
18
  ]
19
19
  license.file = "LICENSE"
@@ -1,11 +1,13 @@
1
1
  from itertools import chain
2
2
 
3
3
  import pytest
4
+ from auditwheel_emscripten.lib_utils import get_all_shared_libs_in_dir
4
5
  from paths import SHAPELY_WHEEL, TEST_DATA
5
6
 
6
7
  from auditwheel_emscripten.repair import copylib, repair, resolve_sharedlib
7
8
  from auditwheel_emscripten.show import show
8
9
  from auditwheel_emscripten.wheel_utils import WHEEL_INFO_RE, unpack
10
+ from auditwheel_emscripten.emscripten_tools.webassembly import parse_dylink_section
9
11
 
10
12
 
11
13
  @pytest.mark.parametrize(
@@ -66,7 +68,7 @@ def test_copylib(tmp_path, wheel_file, expected):
66
68
  ],
67
69
  )
68
70
  def test_repair(tmp_path, wheel_file, expected):
69
- repaired_wheel = repair(wheel_file, TEST_DATA, tmp_path, modify_needed_section=True)
71
+ repaired_wheel = repair(wheel_file, TEST_DATA, tmp_path, modify_rpath=True)
70
72
 
71
73
  libs = show(repaired_wheel)
72
74
  libs_dependencies = list(chain(*libs.values()))
@@ -74,3 +76,41 @@ def test_repair(tmp_path, wheel_file, expected):
74
76
  assert (
75
77
  expected_lib in libs_dependencies
76
78
  ), f"expected lib {expected_lib} not found in dependencies"
79
+
80
+
81
+ @pytest.mark.parametrize(
82
+ "wheel_file, libname, expected",
83
+ [
84
+ (
85
+ SHAPELY_WHEEL,
86
+ [
87
+ "_speedups.cpython-310-wasm32-emscripten.so",
88
+ "_vectorized.cpython-310-wasm32-emscripten.so",
89
+ "libgeos.so.3.10.3",
90
+ "libgeos_c.so",
91
+ ],
92
+ [
93
+ "$ORIGIN/../../Shapely.libs",
94
+ "$ORIGIN/../../Shapely.libs",
95
+ "$ORIGIN",
96
+ "$ORIGIN",
97
+ ],
98
+ ),
99
+ ],
100
+ )
101
+ def test_repair_rpath(tmp_path, wheel_file, libname, expected):
102
+ repaired_wheel = repair(wheel_file, TEST_DATA, tmp_path, modify_rpath=True)
103
+
104
+ # Unpack the wheel and check individual libraries
105
+ extract_dir = unpack(repaired_wheel, tmp_path / "unpacked")
106
+ shared_libs = get_all_shared_libs_in_dir(extract_dir)
107
+
108
+ assert len(shared_libs) > 0, "No shared libraries found in the repaired wheel"
109
+
110
+ # Each shared library should have the correct runtime path
111
+ for lib in shared_libs:
112
+ lib_dylink = parse_dylink_section(lib)
113
+ libname_index = libname.index(lib.name)
114
+ assert (
115
+ expected[libname_index] in lib_dylink.runtime_paths
116
+ ), f"expected runtime path {expected[libname_index]} not found in {lib.name}"
@@ -0,0 +1,200 @@
1
+ version = 1
2
+ revision = 1
3
+ requires-python = ">=3.12"
4
+
5
+ [[package]]
6
+ name = "auditwheel-emscripten"
7
+ source = { editable = "." }
8
+ dependencies = [
9
+ { name = "leb128" },
10
+ { name = "packaging" },
11
+ { name = "pyodide-cli" },
12
+ { name = "typer" },
13
+ { name = "wheel" },
14
+ ]
15
+
16
+ [package.optional-dependencies]
17
+ test = [
18
+ { name = "pytest" },
19
+ ]
20
+
21
+ [package.metadata]
22
+ requires-dist = [
23
+ { name = "leb128" },
24
+ { name = "packaging" },
25
+ { name = "pyodide-cli" },
26
+ { name = "pytest", marker = "extra == 'test'" },
27
+ { name = "typer" },
28
+ { name = "wheel" },
29
+ ]
30
+ provides-extras = ["test"]
31
+
32
+ [[package]]
33
+ name = "click"
34
+ version = "8.1.8"
35
+ source = { registry = "https://pypi.org/simple" }
36
+ dependencies = [
37
+ { name = "colorama", marker = "sys_platform == 'win32'" },
38
+ ]
39
+ sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
40
+ wheels = [
41
+ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
42
+ ]
43
+
44
+ [[package]]
45
+ name = "colorama"
46
+ version = "0.4.6"
47
+ source = { registry = "https://pypi.org/simple" }
48
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
49
+ wheels = [
50
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
51
+ ]
52
+
53
+ [[package]]
54
+ name = "iniconfig"
55
+ version = "2.1.0"
56
+ source = { registry = "https://pypi.org/simple" }
57
+ sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 }
58
+ wheels = [
59
+ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 },
60
+ ]
61
+
62
+ [[package]]
63
+ name = "leb128"
64
+ version = "1.0.8"
65
+ source = { registry = "https://pypi.org/simple" }
66
+ sdist = { url = "https://files.pythonhosted.org/packages/43/3b/476c8bcb181abb060e45bca5ce9b5ba055ea9e2ed3fac6c25b2fe7b9f16b/leb128-1.0.8.tar.gz", hash = "sha256:3a52dca242f93f87a3d766380a93a3fad53ef4044f03018d21705d3b2d9021ee", size = 3361 }
67
+ wheels = [
68
+ { url = "https://files.pythonhosted.org/packages/09/97/45ab4bab1a89e6fdbc822f818bb18b39eed0dd7ed1faac8b89bc6b49a9ed/leb128-1.0.8-py3-none-any.whl", hash = "sha256:76cd271e75ea91aa2fbf7783d60cb7d667b62143d544bcee59159ff258bf4523", size = 3778 },
69
+ ]
70
+
71
+ [[package]]
72
+ name = "markdown-it-py"
73
+ version = "3.0.0"
74
+ source = { registry = "https://pypi.org/simple" }
75
+ dependencies = [
76
+ { name = "mdurl" },
77
+ ]
78
+ sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
79
+ wheels = [
80
+ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
81
+ ]
82
+
83
+ [[package]]
84
+ name = "mdurl"
85
+ version = "0.1.2"
86
+ source = { registry = "https://pypi.org/simple" }
87
+ sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
88
+ wheels = [
89
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
90
+ ]
91
+
92
+ [[package]]
93
+ name = "packaging"
94
+ version = "24.2"
95
+ source = { registry = "https://pypi.org/simple" }
96
+ sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
97
+ wheels = [
98
+ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
99
+ ]
100
+
101
+ [[package]]
102
+ name = "pluggy"
103
+ version = "1.5.0"
104
+ source = { registry = "https://pypi.org/simple" }
105
+ sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
106
+ wheels = [
107
+ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
108
+ ]
109
+
110
+ [[package]]
111
+ name = "pygments"
112
+ version = "2.19.1"
113
+ source = { registry = "https://pypi.org/simple" }
114
+ sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
115
+ wheels = [
116
+ { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
117
+ ]
118
+
119
+ [[package]]
120
+ name = "pyodide-cli"
121
+ version = "0.2.4"
122
+ source = { registry = "https://pypi.org/simple" }
123
+ dependencies = [
124
+ { name = "rich" },
125
+ { name = "typer" },
126
+ ]
127
+ sdist = { url = "https://files.pythonhosted.org/packages/6d/b8/99b0f6e11d505348dde9b45106944156ad5415299ba9ae5a670fad73b2b0/pyodide_cli-0.2.4.tar.gz", hash = "sha256:8cebc6831bfd234b6413d9a73178b93800bd1f6dfa1567514cbfda5768f5095b", size = 10597 }
128
+ wheels = [
129
+ { url = "https://files.pythonhosted.org/packages/9a/b0/f6e406edb166a0996b85bab5e6b9764791768f2a8a3c2ab235943def55e3/pyodide_cli-0.2.4-py3-none-any.whl", hash = "sha256:286585276c7e5c7e7967c317e0432a75271e00c653bc76f836dace3034b2d2bc", size = 11299 },
130
+ ]
131
+
132
+ [[package]]
133
+ name = "pytest"
134
+ version = "8.3.5"
135
+ source = { registry = "https://pypi.org/simple" }
136
+ dependencies = [
137
+ { name = "colorama", marker = "sys_platform == 'win32'" },
138
+ { name = "iniconfig" },
139
+ { name = "packaging" },
140
+ { name = "pluggy" },
141
+ ]
142
+ sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 }
143
+ wheels = [
144
+ { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 },
145
+ ]
146
+
147
+ [[package]]
148
+ name = "rich"
149
+ version = "14.0.0"
150
+ source = { registry = "https://pypi.org/simple" }
151
+ dependencies = [
152
+ { name = "markdown-it-py" },
153
+ { name = "pygments" },
154
+ ]
155
+ sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 }
156
+ wheels = [
157
+ { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 },
158
+ ]
159
+
160
+ [[package]]
161
+ name = "shellingham"
162
+ version = "1.5.4"
163
+ source = { registry = "https://pypi.org/simple" }
164
+ sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 }
165
+ wheels = [
166
+ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 },
167
+ ]
168
+
169
+ [[package]]
170
+ name = "typer"
171
+ version = "0.15.2"
172
+ source = { registry = "https://pypi.org/simple" }
173
+ dependencies = [
174
+ { name = "click" },
175
+ { name = "rich" },
176
+ { name = "shellingham" },
177
+ { name = "typing-extensions" },
178
+ ]
179
+ sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 }
180
+ wheels = [
181
+ { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 },
182
+ ]
183
+
184
+ [[package]]
185
+ name = "typing-extensions"
186
+ version = "4.13.1"
187
+ source = { registry = "https://pypi.org/simple" }
188
+ sdist = { url = "https://files.pythonhosted.org/packages/76/ad/cd3e3465232ec2416ae9b983f27b9e94dc8171d56ac99b345319a9475967/typing_extensions-4.13.1.tar.gz", hash = "sha256:98795af00fb9640edec5b8e31fc647597b4691f099ad75f469a2616be1a76dff", size = 106633 }
189
+ wheels = [
190
+ { url = "https://files.pythonhosted.org/packages/df/c5/e7a0b0f5ed69f94c8ab7379c599e6036886bffcde609969a5325f47f1332/typing_extensions-4.13.1-py3-none-any.whl", hash = "sha256:4b6cf02909eb5495cfbc3f6e8fd49217e6cc7944e145cdda8caa3734777f9e69", size = 45739 },
191
+ ]
192
+
193
+ [[package]]
194
+ name = "wheel"
195
+ version = "0.45.1"
196
+ source = { registry = "https://pypi.org/simple" }
197
+ sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545 }
198
+ wheels = [
199
+ { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494 },
200
+ ]
@@ -1,50 +0,0 @@
1
- name: deploy
2
-
3
- on:
4
- push:
5
-
6
- permissions:
7
- contents: read
8
-
9
- concurrency:
10
- group: main-${{ github.head_ref || github.run_id }}
11
- cancel-in-progress: true
12
-
13
- jobs:
14
- build:
15
- runs-on: ubuntu-latest
16
- if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
17
- steps:
18
- - uses: actions/checkout@v3
19
-
20
- - uses: actions/setup-python@v3
21
-
22
- - name: Install requirements and build wheel
23
- shell: bash -l {0}
24
- run: |
25
- python -m pip install build
26
- python -m build .
27
- - name: Store the distribution packages
28
- uses: actions/upload-artifact@v3
29
- with:
30
- name: python-package-distributions
31
- path: dist/
32
- publish-to-pypi:
33
- name: >-
34
- Publish Python 🐍 distribution 📦 to PyPI
35
- needs:
36
- - build
37
- runs-on: ubuntu-latest
38
- environment:
39
- name: pypi
40
- url: https://pypi.org/p/auditwheel-emscripten
41
- permissions:
42
- id-token: write # IMPORTANT: mandatory for trusted publishing
43
- steps:
44
- - name: Download all the dists
45
- uses: actions/download-artifact@v3
46
- with:
47
- name: python-package-distributions
48
- path: dist/
49
- - name: Publish distribution 📦 to PyPI
50
- uses: pypa/gh-action-pypi-publish@release/v1
@@ -1,25 +0,0 @@
1
- name: test
2
-
3
- on: [push]
4
-
5
- permissions:
6
- contents: read
7
-
8
- concurrency:
9
- group: main-${{ github.head_ref || github.run_id }}
10
- cancel-in-progress: true
11
-
12
- jobs:
13
- test:
14
- runs-on: ubuntu-latest
15
- steps:
16
- - uses: actions/checkout@v3
17
- - uses: actions/setup-python@v3
18
- with:
19
- python-version: "3.10"
20
- - name: Install dependencies
21
- run: |
22
- python3 -m pip install -e .[test]
23
- - name: Run tests
24
- run: |
25
- pytest -v