Skip to content

Commit 7ec208f

Browse files
authored
feat(wheel): Publish single universal wheel, depending on deno package (#425)
1 parent 1d21e18 commit 7ec208f

5 files changed

Lines changed: 414 additions & 177 deletions

File tree

.github/workflows/wheels.yml

Lines changed: 12 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,13 @@ concurrency:
1212
group: ${{ github.workflow }}-${{ github.ref }}
1313
cancel-in-progress: true
1414

15-
env:
16-
MACOSX_DEPLOYMENT_TARGET: '12.0'
17-
1815
defaults:
1916
run:
2017
shell: bash
2118

2219
jobs:
23-
sdist:
24-
name: Build sdist
20+
build:
21+
name: Build packages
2522
runs-on: ubuntu-slim
2623
permissions:
2724
attestations: write
@@ -33,57 +30,11 @@ jobs:
3330
- uses: hynek/build-and-inspect-python-package@d44ca7d91762de7a7d5436ddae667c6da6d1c3df # v2.18.0
3431
with:
3532
attest-build-provenance-github: ${{ github.event_name != 'pull_request' }}
36-
skip-wheel: true
37-
38-
wheel:
39-
name: Build wheel (${{ matrix.os }})
40-
runs-on: ${{ matrix.os }}
41-
needs: [sdist]
42-
strategy:
43-
matrix:
44-
os:
45-
- ubuntu-latest
46-
- ubuntu-24.04-arm
47-
- windows-latest
48-
- macos-15-intel
49-
- macos-latest
50-
fail-fast: false
51-
continue-on-error: true
52-
53-
permissions:
54-
attestations: write
55-
id-token: write
56-
57-
steps:
58-
- name: Download sdist
59-
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
60-
with:
61-
name: Packages
62-
path: dist
63-
64-
- name: Unpack sdist
65-
run: |
66-
tar --strip-components=1 -xzvf dist/*.tar.gz
67-
rm -r dist
68-
69-
- name: Build wheel from sdist
70-
uses: pypa/cibuildwheel@294735312765b09d24a2fbec22660ce817587d55 # v4.1.0
71-
72-
- name: Generate artifact attestation for wheels
73-
if: ${{ github.event_name != 'pull_request' }}
74-
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
75-
with:
76-
subject-path: "wheelhouse/bids_validator_deno-*.whl"
77-
78-
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
79-
with:
80-
name: wheel-${{ matrix.os }}
81-
path: ./wheelhouse
8233

8334
test-wheel:
8435
name: Test wheel (${{ matrix.os }})
8536
runs-on: ${{ matrix.os }}
86-
needs: [wheel]
37+
needs: [build]
8738
strategy:
8839
matrix:
8940
os:
@@ -101,20 +52,17 @@ jobs:
10152
fetch-depth: 1
10253
submodules: true
10354

104-
- name: Download wheel for this platform
55+
- name: Download packages
10556
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
10657
with:
107-
name: wheel-${{ matrix.os }}
108-
path: wheelhouse
58+
name: Packages
59+
path: dist
10960

11061
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
111-
with:
112-
activate-environment: true
113-
python-version: '3'
11462

115-
- name: Install the wheel into a clean environment
63+
- name: Install the wheel into an isolated environment
11664
run: |
117-
uv pip install wheelhouse/*.whl
65+
uv tool install dist/*.whl
11866
11967
- name: Expose a `bids-validator` shim forwarding to bids-validator-deno
12068
run: |
@@ -134,27 +82,17 @@ jobs:
13482
name: Publish to test.pypi.org
13583
if: github.event_name == 'push'
13684
runs-on: ubuntu-latest
137-
needs: [wheel]
85+
needs: [test-wheel]
13886
permissions:
13987
id-token: write
14088

14189
steps:
142-
- name: Download sdist
90+
- name: Download packages
14391
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
14492
with:
14593
name: Packages
14694
path: dist
14795

148-
- name: Download wheels
149-
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
150-
with:
151-
pattern: wheel-*
152-
path: dist
153-
154-
- run: |
155-
mv dist/*/* dist/
156-
rmdir dist/*/
157-
15896
- name: Upload package to PyPI
15997
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
16098
with:
@@ -165,26 +103,16 @@ jobs:
165103
name: Publish
166104
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
167105
runs-on: ubuntu-latest
168-
needs: [wheel]
106+
needs: [test-wheel]
169107
permissions:
170108
id-token: write
171109

172110
steps:
173-
- name: Download sdist
111+
- name: Download packages
174112
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
175113
with:
176114
name: Packages
177115
path: dist
178116

179-
- name: Download wheels
180-
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
181-
with:
182-
pattern: wheel-*
183-
path: dist
184-
185-
- run: |
186-
mv dist/*/* dist/
187-
rmdir dist/*/
188-
189117
- name: Upload package to PyPI
190118
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
### Infrastructure
2+
3+
- The `bids-validator-deno` wheel is now a single universal (`py3-none-any`)
4+
package that depends on the `deno` PyPI package, reducing overhead of supporting
5+
multiple platforms.

pdm_build.py

Lines changed: 92 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,114 @@
11
import json
2-
import shutil
2+
import os
33
import subprocess
4-
import sysconfig
54

5+
from deno import find_deno_bin
6+
7+
BUNDLE_JS = "bids-validator.js"
8+
PERMISSIONS = [
9+
"--allow-read", # read BIDS datasets
10+
"--allow-env", # read environment variables
11+
"--allow-net", # fetch remote schemas/datasets
12+
"--allow-write", # write validation results
13+
"--allow-run=git", # run git to read version info
14+
]
15+
WINDOWS_PERMISSIONS = [
16+
"--allow-sys=osRelease", # Terminal capability detection
17+
]
618

7-
def pdm_build_initialize(context):
8-
context.ensure_build_dir()
919

20+
def pdm_build_initialize(context):
21+
# Version always comes from deno.json (sdist and wheel).
1022
deno_json = context.root / "deno.json"
1123
deno_config = json.loads(deno_json.read_text())
1224
context.config.metadata["version"] = deno_config["version"]
1325

1426
if context.target == "sdist":
1527
return
1628

17-
# Inject compiled binary into scripts/, so it will be picked up to install
18-
target = context.root / "scripts" / "bids-validator-deno"
19-
20-
deno = shutil.which("deno")
21-
if deno is None:
22-
raise OSError("Deno is not installed or not in PATH")
23-
24-
permissions = [
25-
# Access filesystem for reading BIDS datasets
26-
"--allow-read",
27-
# Accept environment variables
28-
"--allow-env",
29-
# Access network for fetching remote schemas
30-
"--allow-net",
31-
# Allow writing results to file
32-
"--allow-write",
33-
# Run git to get version info
34-
"--allow-run=git",
35-
]
36-
if sysconfig.get_platform().startswith("win"):
37-
# Terminal detection code in supports-hyperlinks calls osRelease
38-
permissions.append("--allow-sys=osRelease")
29+
build_dir = context.ensure_build_dir()
30+
package_dir = build_dir / "bids_validator_deno"
31+
package_dir.mkdir(parents=True, exist_ok=True)
32+
33+
bundle = package_dir / BUNDLE_JS
34+
init = package_dir / "__init__.py"
3935

36+
init.write_text(INIT_PY)
37+
38+
deno = os.fsdecode(find_deno_bin())
39+
40+
# `deno bundle` resolves jsr/npm/wasm deps (network at build time) and
41+
# inlines them into a single platform-independent JS file plus a linked
42+
# source map (bundle.js.map). `--frozen` fails rather than silently
43+
# re-resolving if the committed deno.lock is out of date.
4044
subprocess.run(
4145
[
4246
deno,
43-
"compile",
44-
*permissions,
45-
# Types are checked elsewhere. Type checking at wheel build
46-
# is painful if some platforms have newer typescript than others.
47-
"--no-check",
47+
"bundle",
48+
"--frozen",
49+
"--sourcemap",
4850
"-o",
49-
str(target),
51+
str(bundle),
5052
"src/bids-validator.ts",
5153
],
5254
check=True,
55+
cwd=context.root,
5356
)
5457

55-
# Add the current platform tag so the wheel is specific to the OS/architecture
56-
platform_tag = sysconfig.get_platform().replace("-", "_").replace(".", "_")
57-
context.config_settings["--plat-name"] = platform_tag
58+
59+
INIT_PY = f'''\
60+
# This module was generated at build time by pdm_build.py.
61+
#
62+
# This module was adapted from git-annex-wheel/src/git_annex/__init__.py @ commit 9a059be:
63+
#
64+
# https://github.com/psychoinformatics-de/git-annex-wheel/blob/9a059be12db8b90cb1ecf71e498d3a2135366b6f/src/git_annex/__init__.py
65+
# https://hub.datalad.org/git-annex/git-annex-wheel/src/commit/9a059be12db8b90cb1ecf71e498d3a2135366b6f/src/git_annex/__init__.py
66+
#
67+
# By agreement of the authors of that module, releasing this derivative work under MIT is
68+
# authorized. See https://github.com/bids-standard/bids-validator/pull/425.
69+
#
70+
# Changes:
71+
# * Adapted cli() to find Deno bundled in a separate package and exec the local bundle.
72+
#
73+
"""Find the deno runtime and exec the bundled BIDS validator."""
74+
import os
75+
import sys
76+
77+
def cli():
78+
from deno import find_deno_bin
79+
80+
deno = os.fsdecode(find_deno_bin())
81+
windows = sys.platform.startswith('win')
82+
bundle = os.path.join(os.path.dirname(__file__), {BUNDLE_JS!r})
83+
84+
permissions = {PERMISSIONS}
85+
if windows:
86+
permissions.extend({WINDOWS_PERMISSIONS})
87+
88+
argv = [deno, *permissions, bundle, *sys.argv[1:]]
89+
90+
if windows:
91+
exec_subproc(deno, argv)
92+
else:
93+
os.execv(deno, argv)
94+
95+
96+
def exec_subproc(executable, argv):
97+
import subprocess
98+
99+
try:
100+
subprocess.run(
101+
argv,
102+
executable=executable,
103+
shell=False,
104+
check=True,
105+
)
106+
# try flush here to trigger a BrokenPipeError within the try-except block
107+
# (happens if the calling process closed stdout already)
108+
sys.stdout.flush()
109+
except BrokenPipeError:
110+
# setting to None prevents Python from trying to flush again
111+
sys.stdout = None
112+
except subprocess.CalledProcessError as e:
113+
sys.exit(e.returncode)
114+
'''

pyproject.toml

Lines changed: 9 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ description = "Typescript implementation of the BIDS validator"
44
authors = [
55
{name = "bids-standard developers"},
66
]
7-
dependencies = []
7+
dependencies = ["deno>=2.5.0"]
88
readme = "README.md"
9-
license = {text = "MIT"}
9+
requires-python = ">=3.10"
10+
license = "MIT"
11+
license-files = ["LICENSE"]
1012
classifiers = [
1113
"Development Status :: 5 - Production/Stable",
1214
"Intended Audience :: Science/Research",
1315
"Topic :: Scientific/Engineering :: Bio-Informatics",
14-
"License :: OSI Approved :: MIT License",
1516
"Programming Language :: JavaScript",
1617
]
1718
keywords = ["BIDS", "BIDS validator"]
@@ -22,6 +23,9 @@ Documentation = "https://bids-validator.readthedocs.io/"
2223
"Source code" = "https://github.com/bids-standard/bids-validator"
2324
Issues = "https://github.com/bids-standard/bids-validator/issues"
2425

26+
[project.scripts]
27+
bids-validator-deno = "bids_validator_deno:cli"
28+
2529
[dependency-groups]
2630
doc = [
2731
"sphinx",
@@ -32,41 +36,12 @@ doc = [
3236
]
3337

3438
[build-system]
35-
requires = ["pdm-backend"]
39+
requires = ["pdm-backend", "deno>=2.5.0"]
3640
build-backend = "pdm.backend"
3741

3842
[tool.pdm]
3943
distribution = true
4044

4145
[tool.pdm.build]
42-
source-includes = ["src/", "deno.json"]
46+
source-includes = ["src/", "deno.json", "deno.lock"]
4347
excludes = [".*"]
44-
45-
[tool.pdm.build.wheel-data]
46-
scripts = ["scripts/*"]
47-
48-
[tool.cibuildwheel]
49-
build = "cp310-*"
50-
# Deno requires glibc, so the alpine package doesn't give us musllinux yet
51-
# Deno will not build for 32-bit architectures
52-
skip = "*musllinux* *_i686 *-win32"
53-
build-frontend = "build"
54-
55-
manylinux-x86_64-image = "quay.io/pypa/manylinux_2_28_x86_64:latest"
56-
manylinux-aarch64-image = "quay.io/pypa/manylinux_2_28_aarch64:latest"
57-
58-
before-build = "curl -fsSL https://deno.land/install.sh | sh && deno --version"
59-
test-command = "bids-validator-deno --version"
60-
61-
[tool.cibuildwheel.environment]
62-
DENO_INSTALL = "/usr/local"
63-
# Pin Deno version if needed
64-
# deno_version = "v2.5.6"
65-
66-
[[tool.cibuildwheel.overrides]]
67-
select = "*musllinux*"
68-
before-build = 'apk add deno'
69-
70-
[[tool.cibuildwheel.overrides]]
71-
select = "*win*"
72-
before-build = 'npm install -g deno & deno --version'

0 commit comments

Comments
 (0)