Buckets:
| # Copyright 2014 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. | |
| import glob | |
| import hashlib | |
| import importlib.util | |
| import logging | |
| import os | |
| import shutil | |
| import subprocess | |
| import sys | |
| from inspect import signature | |
| from pathlib import Path | |
| from urllib.request import urlopen | |
| from tools import cache, config, shared, system_libs, utils | |
| from tools.settings import settings | |
| from tools.toolchain_profiler import ToolchainProfiler | |
| ports = [] | |
| ports_by_name: dict[str, object] = {} | |
| ports_needed = set() | |
| # Variant builds that we want to support for certain ports | |
| # {variant_name: (port_name, extra_settings)} | |
| port_variants = {} | |
| ports_dir = os.path.dirname(os.path.abspath(__file__)) | |
| logger = logging.getLogger('ports') | |
| def get_port_by_name(name): | |
| port = ports_by_name[name] | |
| if port.is_external: | |
| load_external_port(port) | |
| return ports_by_name[name] | |
| else: | |
| return port | |
| def init_port(name, port): | |
| ports.append(port) | |
| port.is_contrib = name.startswith('contrib.') | |
| port.is_external = hasattr(port, 'EXTERNAL_PORT') | |
| port.name = name | |
| ports_by_name[port.name] = port | |
| if port.is_external: | |
| init_external_port(name, port) | |
| else: | |
| init_local_port(name, port) | |
| def init_local_port(name, port): | |
| if not hasattr(port, 'needed'): | |
| port.needed = lambda s: name in ports_needed | |
| else: | |
| needed = port.needed | |
| port.needed = lambda s: needed(s) or name in ports_needed | |
| if not hasattr(port, 'process_dependencies'): | |
| port.process_dependencies = lambda x: 0 | |
| if not hasattr(port, 'linker_setup'): | |
| port.linker_setup = lambda x, y: 0 | |
| if not hasattr(port, 'deps'): | |
| port.deps = [] | |
| if not hasattr(port, 'process_args'): | |
| port.process_args = lambda x: [] | |
| if not hasattr(port, 'variants'): | |
| # port variants (default: no variants) | |
| port.variants = {} | |
| if not hasattr(port, 'show'): | |
| port.show = lambda: f'{port.name} (--use-port={port.name}; {port.LICENSE})' | |
| for variant, extra_settings in port.variants.items(): | |
| if variant in port_variants: | |
| utils.exit_with_error('duplicate port variant: `%s`' % variant) | |
| port_variants[variant] = (port.name, extra_settings) | |
| validate_port(port) | |
| def load_port_module(module_name, port_file): | |
| spec = importlib.util.spec_from_file_location(module_name, port_file) | |
| port = importlib.util.module_from_spec(spec) | |
| spec.loader.exec_module(port) | |
| return port | |
| def load_external_port(external_port): | |
| name = external_port.name | |
| up_to_date = Ports.fetch_port_artifact(name, external_port.EXTERNAL_PORT, external_port.SHA512) | |
| port_file = os.path.join(Ports.get_dir(), name, external_port.PORT_FILE) | |
| local_port = load_port_module(f'tools.ports.external.{name}', port_file) | |
| ports.remove(external_port) | |
| for a in ['URL', 'DESCRIPTION', 'LICENSE']: | |
| if not hasattr(local_port, a): | |
| setattr(local_port, a, getattr(external_port, a)) | |
| init_port(name, local_port) | |
| if not up_to_date: | |
| Ports.clear_project_build(name) | |
| def init_external_port(name, port): | |
| expected_attrs = ['SHA512', 'PORT_FILE', 'URL', 'DESCRIPTION', 'LICENSE'] | |
| for a in expected_attrs: | |
| assert hasattr(port, a), 'port %s is missing %s' % (port, a) | |
| port.needed = lambda s: name in ports_needed | |
| port.show = lambda: f'{port.name} (--use-port={port.name}; {port.LICENSE})' | |
| def load_port(path, name=None): | |
| if not name: | |
| name = utils.unsuffixed_basename(path) | |
| if name in ports_by_name: | |
| utils.exit_with_error(f'port path [`{path}`] is invalid: duplicate port name `{name}`') | |
| port = load_port_module(f'tools.ports.{name}', path) | |
| init_port(name, port) | |
| return name | |
| def validate_port(port): | |
| expected_attrs = ['get', 'clear', 'show'] | |
| if port.is_contrib: | |
| expected_attrs += ['URL', 'DESCRIPTION', 'LICENSE'] | |
| if hasattr(port, 'handle_options'): | |
| expected_attrs += ['OPTIONS'] | |
| for a in expected_attrs: | |
| assert hasattr(port, a), 'port %s is missing %s' % (port, a) | |
| def read_ports(): | |
| for filename in os.listdir(ports_dir): | |
| if not filename.endswith('.py') or filename == '__init__.py': | |
| continue | |
| load_port(os.path.join(ports_dir, 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 | |
| name = 'contrib.' + utils.unsuffixed(filename) | |
| load_port(os.path.join(contrib_dir, filename), name) | |
| def get_all_files_under(dirname): | |
| for path, _, files in os.walk(dirname): | |
| for name in files: | |
| yield os.path.join(path, name) | |
| def dir_is_newer(dir_a, dir_b): | |
| assert os.path.exists(dir_a) | |
| assert os.path.exists(dir_b) | |
| files_a = ((x, os.path.getmtime(x)) for x in get_all_files_under(dir_a)) | |
| files_b = ((x, os.path.getmtime(x)) for x in get_all_files_under(dir_b)) | |
| newest_a = max(files_a, key=lambda f: f[1]) | |
| newest_b = max(files_b, key=lambda f: f[1]) | |
| logger.debug('newest_a: %s %s', *newest_a) | |
| logger.debug('newest_b: %s %s', *newest_b) | |
| return newest_a[1] > newest_b[1] | |
| def maybe_copy(src, dest): | |
| """Copy a file, but only if the destination is out-of-date. | |
| Just like shutil.copyfile, but will do nothing if the destination already | |
| exists and has the same contents as the source. | |
| In the case where a library is built in multiple different configurations, | |
| we want to avoids racing between processes that are reading headers (without | |
| holding the cache lock) (e.g. normal compile steps) and a process that is | |
| building/installing a new flavor of a given library. In this case the | |
| headers will be "re-installed" but we skip the actual filesystem mods | |
| to avoid racing with other processes that might be reading these files. | |
| """ | |
| if os.path.exists(dest) and utils.read_binary(src) == utils.read_binary(dest): | |
| return | |
| shutil.copyfile(src, dest) | |
| class Ports: | |
| """emscripten-ports library management (https://github.com/emscripten-ports).""" | |
| def get_include_dir(*parts): | |
| dirname = cache.get_include_dir(*parts) | |
| utils.safe_ensure_dirs(dirname) | |
| return dirname | |
| def install_header_dir(src_dir, target=None): | |
| """Like install_headers but recursively copied all files in a directory.""" | |
| if not target: | |
| target = os.path.basename(src_dir) | |
| dest = Ports.get_include_dir(target) | |
| logger.debug(f'installing headers: {dest}') | |
| shutil.copytree(src_dir, dest, dirs_exist_ok=True, copy_function=maybe_copy) | |
| def install_file(filename, target): | |
| sysroot = cache.get_sysroot_dir() | |
| target_dir = os.path.join(sysroot, os.path.dirname(target)) | |
| os.makedirs(target_dir, exist_ok=True) | |
| maybe_copy(filename, os.path.join(sysroot, target)) | |
| def install_headers(src_dir, pattern='*.h', target=None): | |
| logger.debug('install_headers') | |
| dest = Ports.get_include_dir() | |
| assert os.path.exists(dest) | |
| if target: | |
| dest = os.path.join(dest, target) | |
| utils.safe_ensure_dirs(dest) | |
| matches = glob.glob(os.path.join(src_dir, pattern)) | |
| assert matches, f'no headers found to install in {src_dir}' | |
| for f in matches: | |
| logger.debug('installing: ' + os.path.join(dest, os.path.basename(f))) | |
| maybe_copy(f, os.path.join(dest, os.path.basename(f))) | |
| def build_port(src_dir, output_path, port_name, includes=[], flags=[], cxxflags=[], exclude_files=[], exclude_dirs=[], srcs=[]): # noqa | |
| mangled_name = str(Path(output_path).relative_to(Path(cache.get_sysroot(True)) / 'lib')) | |
| mangled_name = mangled_name.replace(os.sep, '_').replace('.a', '').replace('-emscripten', '') | |
| build_dir = os.path.join(Ports.get_build_dir(), port_name, mangled_name) | |
| logger.debug(f'build_port: {port_name} {output_path} in {build_dir}') | |
| if srcs: | |
| srcs = [os.path.join(src_dir, s) for s in srcs] | |
| else: | |
| srcs = [] | |
| for root, dirs, files in os.walk(src_dir): | |
| for ex in exclude_dirs: | |
| if ex in dirs: | |
| dirs.remove(ex) | |
| for f in files: | |
| ext = utils.suffix(f) | |
| if ext in {'.c', '.cpp'} and not any((excluded in f) for excluded in exclude_files): | |
| srcs.append(os.path.join(root, f)) | |
| cflags = system_libs.get_base_cflags(build_dir) + ['-O2', '-I' + src_dir] + flags | |
| for include in includes: | |
| cflags.append('-I' + include) | |
| if system_libs.USE_NINJA: | |
| os.makedirs(build_dir, exist_ok=True) | |
| ninja_file = os.path.join(build_dir, 'build.ninja') | |
| system_libs.ensure_sysroot() | |
| system_libs.create_ninja_file(srcs, ninja_file, output_path, cflags=cflags) | |
| if not os.getenv('EMBUILDER_PORT_BUILD_DEFERRED'): | |
| system_libs.run_ninja(build_dir) | |
| else: | |
| commands = [] | |
| objects = [] | |
| for src in srcs: | |
| relpath = os.path.relpath(src, src_dir) | |
| obj = os.path.join(build_dir, relpath) + '.o' | |
| dirname = os.path.dirname(obj) | |
| os.makedirs(dirname, exist_ok=True) | |
| cmd = [shared.EMCC, '-c', src, '-o', obj] + cflags | |
| if utils.suffix(src) in {'.cc', '.cxx', '.cpp'}: | |
| cmd[0] = shared.EMXX | |
| cmd += cxxflags | |
| commands.append(cmd) | |
| objects.append(obj) | |
| system_libs.run_build_commands(commands, num_inputs=len(srcs)) | |
| system_libs.create_lib(output_path, objects) | |
| return output_path | |
| def get_dir(*parts): | |
| dirname = os.path.join(config.PORTS, *parts) | |
| utils.safe_ensure_dirs(dirname) | |
| return dirname | |
| def erase(): | |
| dirname = Ports.get_dir() | |
| utils.delete_dir(dirname) | |
| def get_build_dir(): | |
| return system_libs.get_build_dir() | |
| name_cache: set[str] = set() | |
| def fetch_port_artifact(name, url, sha512hash=None): | |
| """Fetch the port and return True when the port is up to date, False otherwise.""" | |
| # To compute the sha512 hash, run `curl URL | sha512sum`. | |
| fullname = Ports.get_dir(name) | |
| if name not in Ports.name_cache: # only mention each port once in log | |
| logger.debug(f'including port: {name}') | |
| logger.debug(f' (at {fullname})') | |
| Ports.name_cache.add(name) | |
| # EMCC_LOCAL_PORTS: A hacky way to use a local directory for a port. This | |
| # is not tested but can be useful for debugging | |
| # changes to a port. | |
| # | |
| # if EMCC_LOCAL_PORTS is set, we use a local directory as our ports. This is useful | |
| # for testing. This env var should be in format | |
| # name=dir,name=dir | |
| # e.g. | |
| # sdl2=/home/username/dev/ports/SDL2 | |
| # so you could run | |
| # EMCC_LOCAL_PORTS="sdl2=/home/alon/Dev/ports/SDL2" ./test/runner.py browser.test_sdl2_mouse | |
| # this will simply copy that directory into the ports directory for sdl2, and use that. It also | |
| # clears the build, so that it is rebuilt from that source. | |
| local_ports = os.environ.get('EMCC_LOCAL_PORTS') | |
| if local_ports: | |
| logger.warning('using local ports: %s' % local_ports) | |
| local_ports = [pair.split('=', 1) for pair in local_ports.split(',')] | |
| for local_name, path in local_ports: | |
| if name == local_name: | |
| port = ports_by_name.get(name) | |
| if not port: | |
| utils.exit_with_error('%s is not a known port' % name) | |
| if not hasattr(port, 'SUBDIR'): | |
| utils.exit_with_error(f'port {name} lacks .SUBDIR attribute, which we need in order to override it locally, please update it') | |
| subdir = port.SUBDIR | |
| target = os.path.join(fullname, subdir) | |
| uptodate_message = f'not grabbing local port: {name} from {path} to {fullname} (subdir: {subdir}) as the destination {target} is newer (run emcc --clear-ports if that is incorrect)' | |
| # before acquiring the lock we have an early out if the port already exists | |
| if os.path.exists(target) and dir_is_newer(path, target): | |
| logger.warning(uptodate_message) | |
| return True | |
| with cache.lock('unpack local port'): | |
| # Another early out in case another process unpackage the library while we were | |
| # waiting for the lock | |
| if os.path.exists(target) and not dir_is_newer(path, target): | |
| logger.warning(uptodate_message) | |
| return True | |
| logger.warning(f'grabbing local port: {name} from {path} to {fullname} (subdir: {subdir})') | |
| utils.delete_dir(fullname) | |
| shutil.copytree(path, target) | |
| return False | |
| url_filename = url.rsplit('/')[-1] | |
| ext = url_filename.split('.', 1)[1] | |
| fullpath = fullname + '.' + ext | |
| def retrieve(): | |
| # retrieve from remote server | |
| logger.info(f'retrieving port: {name} from {url}') | |
| if utils.MACOS: | |
| # Use `curl` over `urllib` on macOS to avoid issues with | |
| # certificate verification. | |
| # https://stackoverflow.com/questions/40684543/how-to-make-python-use-ca-certificates-from-mac-os-truststore | |
| # Unlike on Windows or Linux, curl is guaranteed to always be | |
| # available on macOS. | |
| data = subprocess.check_output(['curl', '-sSL', url]) | |
| else: | |
| f = urlopen(url) | |
| data = f.read() | |
| if sha512hash: | |
| actual_hash = hashlib.sha512(data).hexdigest() | |
| if actual_hash != sha512hash: | |
| utils.exit_with_error(f'Unexpected hash: {actual_hash}\n' | |
| 'If you are updating the port, please update the hash.') | |
| utils.write_binary(fullpath, data) | |
| marker = os.path.join(fullname, '.emscripten_url') | |
| def unpack(): | |
| logger.info(f'unpacking port: {name}') | |
| utils.safe_ensure_dirs(fullname) | |
| shutil.unpack_archive(filename=fullpath, extract_dir=fullname) | |
| utils.write_file(marker, url + '\n') | |
| def up_to_date(): | |
| return os.path.exists(marker) and utils.read_file(marker).strip() == url | |
| # before acquiring the lock we have an early out if the port already exists | |
| if up_to_date(): | |
| return True | |
| # main logic. do this under a cache lock, since we don't want multiple jobs to | |
| # retrieve the same port at once | |
| cache.ensure() # TODO: find a better place for this (necessary at the moment) | |
| with cache.lock('unpack port'): | |
| if os.path.exists(fullpath): | |
| # Another early out in case another process unpackage the library while we were | |
| # waiting for the lock | |
| if up_to_date(): | |
| return True | |
| # file exists but tag is bad | |
| logger.warning('local copy of port is not correct, retrieving from remote server') | |
| utils.delete_dir(fullname) | |
| utils.delete_file(fullpath) | |
| retrieve() | |
| unpack() | |
| return False | |
| def fetch_project(name, url, sha512hash=None): | |
| if not Ports.fetch_port_artifact(name, url, sha512hash): | |
| # we unpacked a new version, clear the build in the cache | |
| Ports.clear_project_build(name) | |
| def clear_project_build(name): | |
| port = get_port_by_name(name) | |
| port.clear(Ports, settings, shared) | |
| build_dir = os.path.join(Ports.get_build_dir(), name) | |
| logger.debug(f'clearing port build: {name} {build_dir}') | |
| utils.delete_dir(build_dir) | |
| return build_dir | |
| def write_file(filename, contents): | |
| if os.path.exists(filename) and utils.read_file(filename) == contents: | |
| return | |
| utils.write_file(filename, contents) | |
| def make_pkg_config(name, version, flags): | |
| pkgconfig_dir = cache.get_sysroot_dir('lib/pkgconfig') | |
| filename = os.path.join(pkgconfig_dir, name + '.pc') | |
| Ports.write_file(filename, f''' | |
| Name: {name} | |
| Description: {name} port from emscripten | |
| Version: {version} | |
| Libs: {flags} | |
| Cflags: {flags} | |
| ''') | |
| class OrderedSet: | |
| """Partial implementation of OrderedSet. Just enough for what we need here.""" | |
| def __init__(self, items): | |
| self.dict = {} | |
| for i in items: | |
| self.dict[i] = True | |
| def __repr__(self): | |
| return f"OrderedSet({list(self.dict.keys())})" | |
| def __len__(self): | |
| return len(self.dict.keys()) | |
| def copy(self): | |
| return OrderedSet(self.dict.keys()) | |
| def __iter__(self): | |
| return iter(self.dict.keys()) | |
| def pop(self, index=-1): | |
| key = list(self.dict.keys())[index] | |
| self.dict.pop(key) | |
| return key | |
| def add(self, item): | |
| self.dict[item] = True | |
| def remove(self, item): | |
| del self.dict[item] | |
| def dependency_order(port_list): | |
| # Perform topological sort of ports according to the dependency DAG | |
| port_map = {p.name: p for p in port_list} | |
| # Perform depth first search of dependency graph adding nodes to | |
| # the stack only after all children have been explored. | |
| stack = [] | |
| unsorted = OrderedSet(port_list) | |
| def dfs(node): | |
| for dep in node.deps: | |
| dep, _ = split_port_options(dep) | |
| child = port_map[dep] | |
| if child in unsorted: | |
| unsorted.remove(child) | |
| dfs(child) | |
| stack.append(node) | |
| while unsorted: | |
| dfs(unsorted.pop()) | |
| return stack | |
| def resolve_dependencies(port_set, settings, cflags_only=False): | |
| def add_deps(node): | |
| sig = signature(node.process_dependencies) | |
| if len(sig.parameters) == 2: | |
| # The optional second parameter here is useful for ports that want | |
| # to mutate linker-only settings. Modifying these settings during the | |
| # compile phase (or in a compile-only) command generates errors. | |
| node.process_dependencies(settings, cflags_only) | |
| else: | |
| node.process_dependencies(settings) | |
| for d in node.deps: | |
| d, _ = split_port_options(d) | |
| if d not in ports_by_name: | |
| utils.exit_with_error(f'unknown dependency `{d}` for port `{node.name}`') | |
| dep = get_port_by_name(d) | |
| if dep not in port_set: | |
| port_set.add(dep) | |
| add_deps(dep) | |
| for port in port_set.copy(): | |
| add_deps(port) | |
| def handle_use_port_error(arg, message): | |
| utils.exit_with_error(f'error with `--use-port={arg}` | {message}') | |
| def show_port_help_and_exit(port): | |
| print(port.show()) | |
| if hasattr(port, 'DESCRIPTION'): | |
| print(port.DESCRIPTION) | |
| if hasattr(port, 'OPTIONS'): | |
| print("Options:") | |
| for option, desc in port.OPTIONS.items(): | |
| print(f'* {option}: {desc}') | |
| else: | |
| print("No options.") | |
| if hasattr(port, 'URL'): | |
| print(f'More info: {port.URL}') | |
| sys.exit(0) | |
| # extract dict and delegate to port.handle_options for handling (format is 'option1=value1:option2=value2') | |
| def handle_port_options(name, options, error_handler): | |
| port = get_port_by_name(name) | |
| if options == 'help': | |
| show_port_help_and_exit(port) | |
| if not hasattr(port, 'handle_options'): | |
| error_handler(f'no options available for port `{name}`') | |
| else: | |
| options_dict = {} | |
| for name_value in options.replace('::', '\0').split(':'): | |
| name_value = name_value.replace('\0', ':') | |
| nv = name_value.split('=', 1) | |
| if len(nv) != 2: | |
| error_handler(f'`{name_value}` is missing a value') | |
| if nv[0] not in port.OPTIONS: | |
| error_handler(f'`{nv[0]}` is not supported; available options are {port.OPTIONS}') | |
| if nv[0] in options_dict: | |
| error_handler(f'duplicate option `{nv[0]}`') | |
| options_dict[nv[0]] = nv[1] | |
| port.handle_options(options_dict, error_handler) | |
| # handle port dependencies (ex: deps=['sdl2_image:formats=jpg']) | |
| def handle_port_deps(name, error_handler): | |
| port = get_port_by_name(name) | |
| for dep in port.deps: | |
| dep_name, dep_options = split_port_options(dep) | |
| if dep_name not in ports_by_name: | |
| error_handler(f'unknown dependency `{dep_name}`') | |
| if dep_options: | |
| handle_port_options(dep_name, dep_options, error_handler) | |
| handle_port_deps(dep_name, error_handler) | |
| def split_port_options(arg): | |
| # Ignore ':' in first or second char of string since we could be dealing with a windows drive separator | |
| pos = arg.find(':', 2) | |
| if pos != -1: | |
| return arg[:pos], arg[pos + 1:] | |
| else: | |
| return arg, None | |
| def handle_use_port_arg(settings, arg, error_handler=None): | |
| if not error_handler: | |
| def error_handler(message): | |
| handle_use_port_error(arg, message) | |
| name, options = split_port_options(arg) | |
| if name.endswith('.py'): | |
| port_file_path = name | |
| if not os.path.isfile(port_file_path): | |
| error_handler(f'not a valid port path: {port_file_path}') | |
| name = load_port(port_file_path) | |
| elif name not in ports_by_name: | |
| error_handler(f'invalid port name: `{name}`') | |
| ports_needed.add(name) | |
| if options: | |
| handle_port_options(name, options, error_handler) | |
| handle_port_deps(name, error_handler) | |
| return name | |
| def get_needed_ports(settings, cflags_only=False): | |
| # Start with directly needed ports, and transitively add dependencies | |
| needed = OrderedSet(get_port_by_name(p.name) for p in ports if p.needed(settings)) | |
| resolve_dependencies(needed, settings, cflags_only) | |
| return needed | |
| def build_port(port_name, settings): | |
| port = get_port_by_name(port_name) | |
| port_set = OrderedSet([port]) | |
| resolve_dependencies(port_set, settings) | |
| for port in dependency_order(port_set): | |
| port.get(Ports, settings, shared) | |
| def clear_port(port_name, settings): | |
| port = get_port_by_name(port_name) | |
| port.clear(Ports, settings, shared) | |
| def clear(): | |
| Ports.erase() | |
| def get_libs(settings): | |
| """Return a list of library to link against for the selected ports. | |
| This is called add link time only. | |
| Can have the side effect of building and installing the needed ports. | |
| """ | |
| ret = [] | |
| needed = get_needed_ports(settings) | |
| for port in dependency_order(needed): | |
| port.linker_setup(Ports, settings) | |
| # port.get returns a list of libraries to link | |
| ret += port.get(Ports, settings, shared) | |
| ret.reverse() | |
| return ret | |
| def add_cflags(args, settings): | |
| """Add any compiler flags (e.g -Ifoo) needed by the selected ports. | |
| This is called during the compile phase. Can also add/change settings. | |
| Can have the side effect of building and installing the needed ports. | |
| """ | |
| # Legacy SDL1 port is not actually a port at all but builtin | |
| if settings.USE_SDL == 1: | |
| args += ['-I' + Ports.get_include_dir('SDL')] | |
| needed = get_needed_ports(settings, cflags_only=True) | |
| # Now get (i.e. build) the ports in dependency order. This is important because the | |
| # headers from one port might be needed before we can build the next. | |
| for port in dependency_order(needed): | |
| # When using embuilder, don't build the dependencies | |
| if not os.getenv('EMBUILDER_PORT_BUILD_DEFERRED'): | |
| port.get(Ports, settings, shared) | |
| args += port.process_args(Ports) | |
| def show_ports(): | |
| sorted_ports = sorted(ports, key=lambda p: p.name) | |
| print('Available official ports:') | |
| for port in sorted_ports: | |
| if not port.is_contrib: | |
| print(' ', port.show()) | |
| print('Available contrib ports:') | |
| for port in sorted_ports: | |
| if port.is_contrib: | |
| print(' ', port.show()) | |
| read_ports() | |
Xet Storage Details
- Size:
- 22.9 kB
- Xet hash:
- 0bbc5d3d254fd99bc4d1366a76f32e81247390f1c549e76636d5a576f72c4674
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.