Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add suppport for external ports #21316

Merged
merged 13 commits into from
Feb 14, 2024
71 changes: 71 additions & 0 deletions test/other/external_port_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Copyright 2024 The Emscripten Authors. All rights reserved.
# Emscripten is available under two separate licenses, the MIT license and the
# University of Illinois/NCSA Open Source License. Both these licenses can be
# found in the LICENSE file.
ypujante marked this conversation as resolved.
Show resolved Hide resolved

import os
from typing import Dict, Optional

URL = 'https://github.com/emscripten-core/emscripten'
DESCRIPTION = 'External Ports Test'
LICENSE = 'MIT license'

OPTIONS = {
'value1': 'Value for define TEST_VALUE_1',
'value2': 'Value for define TEST_VALUE_2',
'dependency': 'A dependency'
}

# user options (from --use-port)
opts: Dict[str, Optional[str]] = {
'value1': None,
'value2': None,
'dependency': None
}

EXAMPLE_H = 'int external_port_test_fn(int);'
EXAMPLE_C = 'int external_port_test_fn(int value) { return value; }'
ypujante marked this conversation as resolved.
Show resolved Hide resolved

deps = []


def get_lib_name(settings):
return 'lib_external_port_test.a'


def get(ports, settings, shared):
source_path = os.path.join(ports.get_dir(), 'external_port_test')
os.makedirs(source_path, exist_ok=True)

def create(final):
ports.write_file(os.path.join(source_path, 'external_port_test.h'), EXAMPLE_H)
ports.write_file(os.path.join(source_path, 'external_port_test.c'), EXAMPLE_C)
ports.install_headers(source_path)
print(f'about to build {source_path}')
ports.build_port(source_path, final, 'external_port_test')

return [shared.cache.get_lib(get_lib_name(settings), create, what='port')]


def clear(ports, settings, shared):
shared.cache.erase_lib(get_lib_name(settings))


def process_args(ports):
args = ['-isystem', ports.get_include_dir('external_port_test')]
if opts['value1']:
args.append(f'-DTEST_VALUE_1={opts["value1"]}')
if opts['value2']:
args.append(f'-DTEST_VALUE_2={opts["value2"]}')
if opts['dependency']:
args.append(f'-DTEST_DEPENDENCY_{opts["dependency"].upper()}')
return args


def process_dependencies(settings):
if opts['dependency']:
deps.append(opts['dependency'])


def handle_options(options):
opts.update(options)
33 changes: 33 additions & 0 deletions test/other/test_external_ports.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2024 The Emscripten Authors. All rights reserved.
* Emscripten is available under two separate licenses, the MIT license and the
* University of Illinois/NCSA Open Source License. Both these licenses can be
* found in the LICENSE file.
*/

#include <external_port_test.h>
#include <assert.h>
#include <stdio.h>

#ifdef TEST_DEPENDENCY_SDL2
#include <SDL2/SDL.h>
#endif

// TEST_VALUE_1 and TEST_VALUE_2 are defined via port options
#ifndef TEST_VALUE_1
#define TEST_VALUE_1 0
#endif
#ifndef TEST_VALUE_2
#define TEST_VALUE_2 0
#endif

int main() {
assert(external_port_test_fn(99) == 99); // check that we can call a function from external_port_test.h
printf("value1=%d&value2=%d\n", TEST_VALUE_1, TEST_VALUE_2);
#ifdef TEST_DEPENDENCY_SDL2
SDL_version version;
SDL_VERSION(&version);
printf("sdl2=%d\n", version.major);
#endif
return 0;
}
26 changes: 26 additions & 0 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -2394,6 +2394,32 @@ def test_contrib_ports(self):
# with a different contrib port when there is another one
self.emcc(test_file('other/test_contrib_ports.cpp'), ['--use-port=contrib.glfw3'])

@crossplatform
def test_external_ports(self):
if config.FROZEN_CACHE:
self.skipTest("test doesn't work with frozen cache")
external_port_path = test_file("other/external_port_test.py")
# testing no option
self.emcc(test_file('other/test_external_ports.c'), [f'--use-port={external_port_path}'], output_filename='a0.out.js')
output = self.run_js('a0.out.js')
self.assertContained('value1=0&value2=0\n', output)
# testing 1 option
self.emcc(test_file('other/test_external_ports.c'), [f'--use-port={external_port_path}:value1=12'], output_filename='a1.out.js')
output = self.run_js('a1.out.js')
self.assertContained('value1=12&value2=0\n', output)
self.emcc(test_file('other/test_external_ports.c'), [f'--use-port={external_port_path}:value1=12:value2=36'], output_filename='a2.out.js')
# testing 2 options
output = self.run_js('a2.out.js')
self.assertContained('value1=12&value2=36\n', output)
ypujante marked this conversation as resolved.
Show resolved Hide resolved
# testing dependency
self.emcc(test_file('other/test_external_ports.c'), [f'--use-port={external_port_path}:dependency=sdl2'], output_filename='a3.out.js')
output = self.run_js('a3.out.js')
self.assertContained('sdl2=2\n', output)
# testing invalid dependency
stderr = self.expect_fail([EMCC, test_file('other/test_external_ports.c'), f'--use-port={external_port_path}:dependency=invalid', '-o', 'a4.out.js'])
self.assertFalse(os.path.exists('a4.out.js'))
self.assertContained('Unknown dependency invalid for port external_port_test', stderr)

def test_link_memcpy(self):
# memcpy can show up *after* optimizations, so after our opportunity to link in libc, so it must be special-cased
create_file('main.c', r'''
Expand Down
45 changes: 38 additions & 7 deletions tools/ports/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import os
import shutil
import glob
import importlib.util
import sys
from typing import Set
from tools import cache
from tools import config
Expand All @@ -33,8 +35,7 @@
logger = logging.getLogger('ports')


def load_port(name):
port = __import__(name, globals(), level=1, fromlist=[None])
def init_port(name, port):
ports.append(port)
port.is_contrib = name.startswith('contrib.')
port.name = name
Expand Down Expand Up @@ -64,6 +65,22 @@ def load_port(name):
port_variants[variant] = (port.name, extra_settings)


def load_port_by_name(name):
port = __import__(name, globals(), level=1, fromlist=[None])
init_port(name, port)


def load_port_by_path(path):
name = os.path.splitext(os.path.basename(path))[0]
module_name = f'tools.ports.{name}'
sbc100 marked this conversation as resolved.
Show resolved Hide resolved
spec = importlib.util.spec_from_file_location(module_name, path)
port = importlib.util.module_from_spec(spec)
sys.modules[module_name] = port
spec.loader.exec_module(port)
ypujante marked this conversation as resolved.
Show resolved Hide resolved
init_port(name, port)
return port


def validate_port(port):
expected_attrs = ['get', 'clear', 'show']
if port.is_contrib:
Expand All @@ -88,14 +105,14 @@ def read_ports():
if not filename.endswith('.py') or filename == '__init__.py':
continue
filename = os.path.splitext(filename)[0]
load_port(filename)
load_port_by_name(filename)

contrib_dir = os.path.join(ports_dir, 'contrib')
for filename in os.listdir(contrib_dir):
if not filename.endswith('.py') or filename == '__init__.py':
continue
filename = os.path.splitext(filename)[0]
load_port('contrib.' + filename)
load_port_by_name('contrib.' + filename)
ypujante marked this conversation as resolved.
Show resolved Hide resolved

validate_ports()

Expand Down Expand Up @@ -386,6 +403,8 @@ def resolve_dependencies(port_set, settings):
def add_deps(node):
node.process_dependencies(settings)
for d in node.deps:
if d not in ports_by_name:
utils.exit_with_error(f'Unknown dependency {d} for port {node.name}')
ypujante marked this conversation as resolved.
Show resolved Hide resolved
dep = ports_by_name[d]
if dep not in port_set:
port_set.add(dep)
Expand All @@ -400,11 +419,23 @@ def handle_use_port_error(arg, message):


def handle_use_port_arg(settings, arg):
args = arg.split(':', 1)
name, options = args[0], None
if arg.find(':\\') == 1: # detect windows path C:\path\port.py
drive, rest = arg[:2], arg[2:] # drive=C:, rest=\path\port.py
args = rest.split(':', 1)
name, options = drive + args[0], None
ypujante marked this conversation as resolved.
Show resolved Hide resolved
else:
args = arg.split(':', 1)
name, options = args[0], None
if len(args) == 2:
options = args[1]
if name not in ports_by_name:
if name.endswith('.py'):
port_file_path = name
if not os.path.isfile(port_file_path):
handle_use_port_error(arg, f'not a valid port path: {port_file_path}')
port = load_port_by_path(port_file_path)
validate_port(port)
name = port.name
elif name not in ports_by_name:
handle_use_port_error(arg, f'invalid port name: {name}')
ports_needed.add(name)
if options:
Expand Down
Loading