#!/usr/bin/env python3 import json import os import platform import shutil import subprocess import sys import tempfile import unittest WINDOWS = sys.platform.startswith('win') MACOS = sys.platform == 'darwin' MACOS_ARM64 = MACOS and platform.machine() == 'arm64' emconfig = os.path.abspath('.emscripten') assert os.path.exists(emconfig) upstream_emcc = os.path.join('upstream', 'emscripten', 'emcc') emsdk = './emsdk' if WINDOWS: upstream_emcc += '.bat' emsdk = 'emsdk.bat' else: emsdk = './emsdk' # Utilities def listify(x): if type(x) in {list, tuple}: return x return [x] def copy_emsdk_to(targetdir): for filename in os.listdir('.'): if not filename.startswith('.') and not os.path.isdir(filename): shutil.copy2(filename, os.path.join(targetdir, filename)) def check_call(cmd, **kwargs): if type(cmd) is not list: cmd = cmd.split() print('running: %s' % cmd) subprocess.run(cmd, check=True, text=True, **kwargs) def checked_call_with_output(cmd, expected=None, unexpected=None, stderr=None, env=None): cmd = cmd.split(' ') print('running: %s' % cmd) try: stdout = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=stderr, check=True, text=True, env=env).stdout except subprocess.CalledProcessError as e: print(e.stderr) print(e.stdout) raise e if expected: for x in listify(expected): assert x in stdout, 'expected output missing: ' + stdout + '\n[[[' + x + ']]]' if unexpected: for x in listify(unexpected): assert x not in stdout, 'unexpected output present: ' + stdout + '\n[[[' + x + ']]]' def failing_call_with_output(cmd, expected, env=None): proc = subprocess.run(cmd.split(' '), capture_output=True, text=True, env=env) stdout = proc.stdout stderr = proc.stderr if WINDOWS: print('warning: skipping part of failing_call_with_output() due to error codes not being propagated (see #592)') else: assert proc.returncode, 'call must have failed: ' + str([stdout, '\n========\n', stderr]) assert expected in stdout or expected in stderr, 'call did not have the expected output: %s: %s' % (expected, str([stdout, '\n========\n', stderr])) def hack_emsdk(marker, replacement): with open('emsdk.py') as f: src = f.read() assert marker in src src = src.replace(marker, replacement) name = '__test_emsdk' with open(name, 'w') as f: f.write(src) return name def get_longest_path_in_dir(dirname): longest = '' for root, _dirs, files in os.walk(dirname): for f in files: fullname = os.path.join(root, f) if len(fullname) > len(longest): longest = fullname return longest # Set up TAGS = json.loads(open('emscripten-releases-tags.json').read()) # Tests def do_lib_building(emcc): cache_building_messages = ['generating system library: '] def do_build(args, is_expected=None): unexpected = None expected = None if is_expected is True: expected = cache_building_messages elif is_expected is False: unexpected = cache_building_messages checked_call_with_output(emcc + ' hello_world.c' + args, expected=expected, unexpected=unexpected, stderr=subprocess.STDOUT) # The emsdk ships all system libraries so we don't expect to see any # cache population unless we explicly --clear-cache. do_build('', is_expected=False) check_call(emcc + ' --clear-cache') do_build(' -O2', is_expected=True) # Do another build at -O0. In nwers SDK versions this generates # different libs, but not in older ones so don't assert here. do_build('') # Now verify that libs are *not* build do_build(' -s WASM=0', is_expected=False) do_build(' -O2 -s WASM=0', is_expected=False) def run_emsdk(cmd): if type(cmd) is not list: cmd = cmd.split() check_call([emsdk] + cmd) class Emsdk(unittest.TestCase): @classmethod def setUpClass(cls): with open('hello_world.c', 'w') as f: f.write('''\ #include int main() { printf("Hello, world!\\n"); return 0; } ''') def setUp(self): run_emsdk('install latest') run_emsdk('activate latest') def test_extrememly_long_filenames(self): # We have special support for filenames longer than 256 on windows. This # test installs emsdk in path that exceeds this limit in order to test # this handling. longpath = os.path.abspath('very_long_filename_indeed') additional = 140 - len(longpath) longpath += 'x' * additional if os.path.exists(longpath): # shutil.rmtree requires the special long path prefix longpath_with_prefix = '\\\\?\\' + longpath assert os.path.exists(longpath_with_prefix) shutil.rmtree(longpath_with_prefix) os.makedirs(longpath) copy_emsdk_to(longpath) emsdk = os.path.join(longpath, 'emsdk') if WINDOWS: emsdk += '.bat' self.assertTrue(os.path.exists(emsdk)) check_call([emsdk, 'install', 'latest']) check_call([emsdk, 'activate', 'latest']) # Check that emcc exists in the expected location emcc = os.path.join(longpath, 'upstream', 'emscripten', 'emcc') if WINDOWS: emcc += '.bat' print(emcc) self.assertTrue(os.path.exists(emcc)) # Find the longest installed path and assert this is longer than 256 longest_path = get_longest_path_in_dir(longpath) print(f'longest ({len(longest_path)}: {longest_path})') self.assertGreater(len(longest_path), 256) # Finally make sure we can actaully compile something check_call([emcc, 'hello_world.c']) def test_unknown_arch(self): env = os.environ.copy() env['EMSDK_ARCH'] = 'mips' failing_call_with_output(emsdk + ' install latest', expected='unknown machine architecture: mips', env=env) def test_wrong_bitness(self): env = os.environ.copy() env['EMSDK_ARCH'] = 'x86' failing_call_with_output(emsdk + ' install sdk-latest-64bit', expected='is only provided for 64-bit OSe', env=env) def test_already_installed(self): # Test we don't re-download unnecessarily checked_call_with_output(emsdk + ' install latest', expected='already installed', unexpected='Downloading:') def test_list(self): # Test we report installed tools properly. The latest version should be # installed, but not some random old one. checked_call_with_output(emsdk + ' list', expected=TAGS['aliases']['latest'] + ' INSTALLED', unexpected='1.39.15 INSTALLED:') def test_config_contents(self): print('test .emscripten contents') with open(emconfig) as f: config = f.read() assert 'upstream' in config def test_lib_building(self): print('building proper system libraries') do_lib_building(upstream_emcc) def test_redownload(self): print('go back to using upstream') run_emsdk('activate latest') # Test the normal tools like node don't re-download on re-install print('another install must re-download') checked_call_with_output(emsdk + ' uninstall node-22.16.0-64bit') checked_call_with_output(emsdk + ' install node-22.16.0-64bit', expected='Downloading:', unexpected='already installed') checked_call_with_output(emsdk + ' install node-22.16.0-64bit', unexpected='Downloading:', expected='already installed') def test_tot_upstream(self): print('test update-tags') run_emsdk('update-tags') print('test tot-upstream') run_emsdk('install tot-upstream') with open(emconfig) as f: config = f.read() run_emsdk('activate tot-upstream') with open(emconfig + '.old') as f: old_config = f.read() self.assertEqual(config, old_config) # TODO; test on latest as well check_call(upstream_emcc + ' hello_world.c') def test_closure(self): # Specifically test with `--closure` so we know that node_modules is working check_call(upstream_emcc + ' hello_world.c --closure=1') def test_specific_version(self): if MACOS_ARM64: self.skipTest('Old sdk versions do not have ARM64 binaries') print('test specific release (new, short name)') run_emsdk('install 1.38.33') print('another install, but no need for re-download') checked_call_with_output(emsdk + ' install 1.38.33', expected='Skipped', unexpected='Downloading:') run_emsdk('activate 1.38.33') def test_specific_version_full(self): if MACOS_ARM64: self.skipTest('Old sdk versions do not have ARM64 binaries') print('test specific release (new, full name)') run_emsdk('install sdk-1.38.33-64bit') run_emsdk('activate sdk-1.38.33-64bit') print('test specific release (new, tag name)') run_emsdk('install sdk-tag-1.38.33-64bit') run_emsdk('activate sdk-tag-1.38.33-64bit') def test_binaryen_from_source(self): if WINDOWS: # It takes over 30 mins to build binaryen using Visual Studio in CI self.skipTest('test is too slow under windows') run_emsdk(['install', '--build=Release', 'binaryen-main-64bit']) def test_no_32bit(self): print('test 32-bit error') emsdk_hacked = hack_emsdk('not is_os_64bit()', 'True') failing_call_with_output('%s %s install latest' % (sys.executable, emsdk_hacked), 'this tool is only provided for 64-bit OSes') os.remove(emsdk_hacked) def test_update_no_git(self): print('test non-git update') temp_dir = tempfile.mkdtemp() copy_emsdk_to(temp_dir) olddir = os.getcwd() try: os.chdir(temp_dir) run_emsdk('update') print('second time') run_emsdk('update') finally: os.chdir(olddir) def test_install_arbitrary(self): # Test that its possible to install arbrary emscripten-releases SDKs run_emsdk('install 1b7f7bc6002a3ca73647f41fc10e1fac7f06f804') # Check that its not re-downloaded checked_call_with_output(emsdk + ' install 1b7f7bc6002a3ca73647f41fc10e1fac7f06f804', expected='Skipped', unexpected='Downloading:') def test_install_tool(self): # Test that its possible to install emscripten as tool instead of SDK checked_call_with_output(emsdk + ' install releases-77b065ace39e6ab21446e13f92897f956c80476a', unexpected='Installing SDK') def test_activate_missing(self): run_emsdk('install latest') failing_call_with_output(emsdk + ' activate 2.0.1', expected="error: tool is not installed and therefore cannot be activated: 'releases-13e29bd55185e3c12802bc090b4507901856b2ba-64bit'") def test_keep_downloads(self): env = os.environ.copy() env['EMSDK_KEEP_DOWNLOADS'] = '1' # With EMSDK_KEEP_DOWNLOADS the downloading should happen on the first # install of 2.0.28, and again when we install 2.0.29, but not on the # second install of 2.0.28 because the zip should already be local. shutil.rmtree('downloads') checked_call_with_output(emsdk + ' install 3.1.54', expected='Downloading:', env=env) checked_call_with_output(emsdk + ' install 3.1.55', expected='Downloading:', env=env) checked_call_with_output(emsdk + ' install 3.1.54', expected='already downloaded, skipping', unexpected='Downloading:', env=env) if __name__ == '__main__': unittest.main(verbosity=2)