Skip to content

Commit 26b4bce

Browse files
authored
Allow externally hosted contrib ports (#24303)
The logic that this PR implements is the following: * the external port is loaded like the other ports and "flagged" as being external (`port.is_external = hasattr(port, 'EXTERNAL_PORT'`) * if the port is never needed, then nothing else happens * if the port is needed (because the user wants to use it via `--use-port=contrib.emdawn`, or it is a dependency of another port), then the code fetches the artifact and uses the port file included in the artifact to load the actual port file as if it was a local port, and replaces it in the ports array and `ports_by_name` map. => as a result, all calls using `ports_by_name` have been changed to call `get_port_by_name` to detect whether the port is remote or not and act appropriately An external port looks like this: it only contains info about where the port is located, the actual logic is coming from `PORT_FILE` ``` TAG = 'v20250509.171557' EXTERNAL_PORT = f'https://github.com/google/dawn/releases/download/{TAG}/emdawnwebgpu_pkg-{TAG}.zip' SHA512 = '4b66bf0f64b9616a6420abdad636b3ecefab892dde8f67cd941147bfddf7920f5523ff10160c9a563ef377a0f88b2dfc033527591b2d0753d531de5cbbabde59' PORT_FILE = 'emdawnwebgpu_pkg/emdawnwebgpu.port.py' # contrib port information (required) URL = 'https://dawn.googlesource.com/dawn' DESCRIPTION = 'Dawn is an open-source and cross-platform implementation of the WebGPU standard' LICENSE = 'BSD 3-Clause License' ```
1 parent 94ce461 commit 26b4bce

File tree

3 files changed

+89
-20
lines changed

3 files changed

+89
-20
lines changed

test/test_other.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2592,6 +2592,10 @@ def test_contrib_ports(self):
25922592
# with a different contrib port when there is another one
25932593
self.emcc(test_file('other/test_contrib_ports.cpp'), ['--use-port=contrib.glfw3'])
25942594

2595+
@requires_network
2596+
def test_remote_ports(self):
2597+
self.emcc(test_file('hello_world.c'), ['--use-port=contrib.emdawnwebgpu'])
2598+
25952599
@crossplatform
25962600
def test_external_ports_simple(self):
25972601
if config.FROZEN_CACHE:

tools/ports/__init__.py

Lines changed: 69 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from inspect import signature
1414
import sys
1515
import subprocess
16-
from typing import Set
16+
from typing import Set, Dict
1717
from urllib.request import urlopen
1818

1919
from tools import cache
@@ -26,7 +26,7 @@
2626

2727
ports = []
2828

29-
ports_by_name = {}
29+
ports_by_name: Dict[str, object] = {}
3030

3131
ports_needed = set()
3232

@@ -39,11 +39,28 @@
3939
logger = logging.getLogger('ports')
4040

4141

42+
def get_port_by_name(name):
43+
port = ports_by_name[name]
44+
if port.is_external:
45+
load_external_port(port)
46+
return ports_by_name[name]
47+
else:
48+
return port
49+
50+
4251
def init_port(name, port):
4352
ports.append(port)
4453
port.is_contrib = name.startswith('contrib.')
54+
port.is_external = hasattr(port, 'EXTERNAL_PORT')
4555
port.name = name
4656
ports_by_name[port.name] = port
57+
if port.is_external:
58+
init_external_port(name, port)
59+
else:
60+
init_local_port(name, port)
61+
62+
63+
def init_local_port(name, port):
4764
if not hasattr(port, 'needed'):
4865
port.needed = lambda s: name in ports_needed
4966
else:
@@ -71,15 +88,41 @@ def init_port(name, port):
7188
validate_port(port)
7289

7390

91+
def load_port_module(module_name, port_file):
92+
spec = importlib.util.spec_from_file_location(module_name, port_file)
93+
port = importlib.util.module_from_spec(spec)
94+
spec.loader.exec_module(port)
95+
return port
96+
97+
98+
def load_external_port(external_port):
99+
name = external_port.name
100+
up_to_date = Ports.fetch_port_artifact(name, external_port.EXTERNAL_PORT, external_port.SHA512)
101+
port_file = os.path.join(Ports.get_dir(), name, external_port.PORT_FILE)
102+
local_port = load_port_module(f'tools.ports.external.{name}', port_file)
103+
ports.remove(external_port)
104+
for a in ['URL', 'DESCRIPTION', 'LICENSE']:
105+
if not hasattr(local_port, a):
106+
setattr(local_port, a, getattr(external_port, a))
107+
init_port(name, local_port)
108+
if not up_to_date:
109+
Ports.clear_project_build(name)
110+
111+
112+
def init_external_port(name, port):
113+
expected_attrs = ['SHA512', 'PORT_FILE', 'URL', 'DESCRIPTION', 'LICENSE']
114+
for a in expected_attrs:
115+
assert hasattr(port, a), 'port %s is missing %s' % (port, a)
116+
port.needed = lambda s: name in ports_needed
117+
port.show = lambda: f'{port.name} (--use-port={port.name}; {port.LICENSE})'
118+
119+
74120
def load_port(path, name=None):
75121
if not name:
76122
name = shared.unsuffixed_basename(path)
77123
if name in ports_by_name:
78124
utils.exit_with_error(f'port path [`{path}`] is invalid: duplicate port name `{name}`')
79-
module_name = f'tools.ports.{name}'
80-
spec = importlib.util.spec_from_file_location(module_name, path)
81-
port = importlib.util.module_from_spec(spec)
82-
spec.loader.exec_module(port)
125+
port = load_port_module(f'tools.ports.{name}', path)
83126
init_port(name, port)
84127
return name
85128

@@ -251,7 +294,8 @@ def get_build_dir():
251294
name_cache: Set[str] = set()
252295

253296
@staticmethod
254-
def fetch_project(name, url, sha512hash=None):
297+
def fetch_port_artifact(name, url, sha512hash=None):
298+
"""This function only fetches the port and returns True when the port is up to date, False otherwise"""
255299
# To compute the sha512 hash, run `curl URL | sha512sum`.
256300
fullname = Ports.get_dir(name)
257301

@@ -291,18 +335,17 @@ def fetch_project(name, url, sha512hash=None):
291335
# before acquiring the lock we have an early out if the port already exists
292336
if os.path.exists(target) and dir_is_newer(path, target):
293337
logger.warning(uptodate_message)
294-
return
338+
return True
295339
with cache.lock('unpack local port'):
296340
# Another early out in case another process unpackage the library while we were
297341
# waiting for the lock
298342
if os.path.exists(target) and not dir_is_newer(path, target):
299343
logger.warning(uptodate_message)
300-
return
344+
return True
301345
logger.warning(f'grabbing local port: {name} from {path} to {fullname} (subdir: {subdir})')
302346
utils.delete_dir(fullname)
303347
shutil.copytree(path, target)
304-
Ports.clear_project_build(name)
305-
return
348+
return False
306349

307350
url_filename = url.rsplit('/')[-1]
308351
ext = url_filename.split('.', 1)[1]
@@ -343,16 +386,17 @@ def up_to_date():
343386

344387
# before acquiring the lock we have an early out if the port already exists
345388
if up_to_date():
346-
return
389+
return True
347390

348391
# main logic. do this under a cache lock, since we don't want multiple jobs to
349392
# retrieve the same port at once
393+
cache.ensure() # TODO: find a better place for this (necessary at the moment)
350394
with cache.lock('unpack port'):
351395
if os.path.exists(fullpath):
352396
# Another early out in case another process unpackage the library while we were
353397
# waiting for the lock
354398
if up_to_date():
355-
return
399+
return True
356400
# file exists but tag is bad
357401
logger.warning('local copy of port is not correct, retrieving from remote server')
358402
utils.delete_dir(fullname)
@@ -361,12 +405,17 @@ def up_to_date():
361405
retrieve()
362406
unpack()
363407

408+
return False
409+
410+
@staticmethod
411+
def fetch_project(name, url, sha512hash=None):
412+
if not Ports.fetch_port_artifact(name, url, sha512hash):
364413
# we unpacked a new version, clear the build in the cache
365414
Ports.clear_project_build(name)
366415

367416
@staticmethod
368417
def clear_project_build(name):
369-
port = ports_by_name[name]
418+
port = get_port_by_name(name)
370419
port.clear(Ports, settings, shared)
371420
build_dir = os.path.join(Ports.get_build_dir(), name)
372421
logger.debug(f'clearing port build: {name} {build_dir}')
@@ -449,7 +498,7 @@ def add_deps(node):
449498
d, _ = split_port_options(d)
450499
if d not in ports_by_name:
451500
utils.exit_with_error(f'unknown dependency `{d}` for port `{node.name}`')
452-
dep = ports_by_name[d]
501+
dep = get_port_by_name(d)
453502
if dep not in port_set:
454503
port_set.add(dep)
455504
add_deps(dep)
@@ -479,7 +528,7 @@ def show_port_help_and_exit(port):
479528

480529
# extract dict and delegate to port.handle_options for handling (format is 'option1=value1:option2=value2')
481530
def handle_port_options(name, options, error_handler):
482-
port = ports_by_name[name]
531+
port = get_port_by_name(name)
483532
if options == 'help':
484533
show_port_help_and_exit(port)
485534
if not hasattr(port, 'handle_options'):
@@ -501,7 +550,7 @@ def handle_port_options(name, options, error_handler):
501550

502551
# handle port dependencies (ex: deps=['sdl2_image:formats=jpg'])
503552
def handle_port_deps(name, error_handler):
504-
port = ports_by_name[name]
553+
port = get_port_by_name(name)
505554
for dep in port.deps:
506555
dep_name, dep_options = split_port_options(dep)
507556
if dep_name not in ports_by_name:
@@ -541,21 +590,21 @@ def error_handler(message):
541590

542591
def get_needed_ports(settings, cflags_only=False):
543592
# Start with directly needed ports, and transitively add dependencies
544-
needed = OrderedSet(p for p in ports if p.needed(settings))
593+
needed = OrderedSet(get_port_by_name(p.name) for p in ports if p.needed(settings))
545594
resolve_dependencies(needed, settings, cflags_only)
546595
return needed
547596

548597

549598
def build_port(port_name, settings):
550-
port = ports_by_name[port_name]
599+
port = get_port_by_name(port_name)
551600
port_set = OrderedSet([port])
552601
resolve_dependencies(port_set, settings)
553602
for port in dependency_order(port_set):
554603
port.get(Ports, settings, shared)
555604

556605

557606
def clear_port(port_name, settings):
558-
port = ports_by_name[port_name]
607+
port = get_port_by_name(port_name)
559608
port.clear(Ports, settings, shared)
560609

561610

tools/ports/contrib/emdawnwebgpu.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copyright 2025 The Emscripten Authors. All rights reserved.
2+
# Emscripten is available under two separate licenses, the MIT license and the
3+
# University of Illinois/NCSA Open Source License. Both these licenses can be
4+
# found in the LICENSE file.
5+
6+
7+
TAG = 'v20250516.124039'
8+
9+
EXTERNAL_PORT = f'https://github.com/google/dawn/releases/download/{TAG}/emdawnwebgpu_pkg-{TAG}.zip'
10+
SHA512 = '994eac4be5f69d8ec83838af9c7b4cc87f15fa22bede589517c169320dd69ab5cf164528f7bd6ec6503b1ef178da3d87df0565d16445dac2a69f98450083dd8f'
11+
PORT_FILE = 'emdawnwebgpu_pkg/emdawnwebgpu.port.py'
12+
13+
# contrib port information (required)
14+
URL = 'https://dawn.googlesource.com/dawn'
15+
DESCRIPTION = 'Dawn is an open-source and cross-platform implementation of the WebGPU standard'
16+
LICENSE = 'Some files: BSD 3-Clause License. Other files: Emscripten\'s license (available under both MIT License and University of Illinois/NCSA Open Source License)'

0 commit comments

Comments
 (0)