Adding python types, part 3. NFC

This change enabled `disallow_untyped_defs` which forces all functions
to be typed, but initially only for `tools/shared.py`.

This process uncovered a typo in the exception path of
`get_clang_targets`.
diff --git a/.mypy.ini b/.mypy.ini
index ec5da26..9a3de07 100644
--- a/.mypy.ini
+++ b/.mypy.ini
@@ -2,6 +2,7 @@
 mypy_path = third_party/,third_party/ply,third_party/websockify
 files = .
 exclude = (?x)(
+  cache/ |
   third_party |
   conf\.py |
   emrun\.py |
@@ -10,5 +11,8 @@
   test/parse_benchmark_output\.py
  )
 
-[mypy-tools.create_dom_pk_codes,tools.webidl_binder,tools.toolchain_profiler,tools.filelock,tools.find_bigvars,leb128,ply.*]
+[mypy-tools.create_dom_pk_codes,tools.webidl_binder,tools.toolchain_profiler,tools.find_bigvars,leb128,ply.*]
 ignore_errors = True
+
+[mypy-embuilder,tools.shared]
+disallow_untyped_defs = True
diff --git a/embuilder.py b/embuilder.py
index 8281c45..86a6d20 100755
--- a/embuilder.py
+++ b/embuilder.py
@@ -16,6 +16,7 @@
 import logging
 import sys
 import time
+from typing import Generator, List, Tuple
 from contextlib import contextmanager
 
 from tools import cache
@@ -107,7 +108,7 @@
 }
 
 
-def get_help():
+def get_help() -> str:
   all_tasks = get_system_tasks()[1] + PORTS
   all_tasks.sort()
   return '''
@@ -120,7 +121,7 @@
 
 
 @contextmanager
-def get_port_variant(name):
+def get_port_variant(name: str) -> Generator:
   if name in ports.port_variants:
     name, extra_settings = ports.port_variants[name]
     old_settings = settings.dict().copy()
@@ -135,17 +136,17 @@
     settings.dict().update(old_settings)
 
 
-def clear_port(port_name):
+def clear_port(port_name: str) -> None:
   with get_port_variant(port_name) as port_name:
     ports.clear_port(port_name, settings)
 
 
-def build_port(port_name):
+def build_port(port_name: str) -> None:
   with get_port_variant(port_name) as port_name:
     ports.build_port(port_name, settings)
 
 
-def get_system_tasks():
+def get_system_tasks() -> Tuple[List[str], List[str]]:
   system_libraries = system_libs.Library.get_all_variations()
   system_tasks = list(system_libraries.keys())
   # This is needed to build the generated_struct_info.json file.
@@ -156,7 +157,7 @@
   return system_libraries, system_tasks
 
 
-def main():
+def main() -> int:
   all_build_start_time = time.time()
 
   parser = argparse.ArgumentParser(description=__doc__,
diff --git a/test/common.py b/test/common.py
index ca3ba1a..5dab94c 100644
--- a/test/common.py
+++ b/test/common.py
@@ -421,6 +421,7 @@
 class RunnerCore(unittest.TestCase, metaclass=RunnerMeta):
   # default temporary directory settings. set_temp_dir may be called later to
   # override these
+  assert shared.TEMP_DIR
   temp_dir = shared.TEMP_DIR
   canonical_temp_dir = get_canonical_temp_dir(shared.TEMP_DIR)
 
diff --git a/test/sockets/socket_relay.py b/test/sockets/socket_relay.py
index 06fcc57..7b3c930 100644
--- a/test/sockets/socket_relay.py
+++ b/test/sockets/socket_relay.py
@@ -24,7 +24,7 @@
 
 
 class Listener(threading.Thread):
-  other: Optional[Listener] = None  # noqa: F821
+  other: Optional['Listener'] = None
 
   def run(self):
     self.conn = None
diff --git a/tools/building.py b/tools/building.py
index 7c3d496..e30e350 100644
--- a/tools/building.py
+++ b/tools/building.py
@@ -17,6 +17,7 @@
 from subprocess import PIPE
 
 from . import cache
+from . import js_optimizer
 from . import diagnostics
 from . import response_file
 from . import shared
@@ -363,8 +364,8 @@
     return f'-O{min(opt_level, 3)}'
 
 
-def js_optimizer(filename, passes):
-  from . import js_optimizer
+@ToolchainProfiler.profile()
+def run_js_optimizer(filename, passes):
   try:
     return js_optimizer.run(filename, passes)
   except subprocess.CalledProcessError as e:
@@ -372,7 +373,7 @@
 
 
 # run JS optimizer on some JS, ignoring asm.js contents if any - just run on it all
-def acorn_optimizer(filename, passes, extra_info=None, return_output=False):
+def acorn_optimizer(filename, passes, extra_info=None, return_output=False) -> str:
   optimizer = path_from_root('tools/acorn-optimizer.js')
   original_filename = filename
   if extra_info is not None:
@@ -382,6 +383,7 @@
     with open(temp, 'a') as f:
       f.write('// EXTRA_INFO: ' + extra_info)
     filename = temp
+  assert config.NODE_JS
   cmd = config.NODE_JS + [optimizer, filename] + passes
   # Keep JS code comments intact through the acorn optimization pass so that JSDoc comments
   # will be carried over to a later Closure run.
@@ -394,11 +396,11 @@
   if return_output:
     return check_call(cmd, stdout=PIPE).stdout
 
-  acorn_optimizer.counter += 1
+  acorn_optimizer.counter += 1  # type: ignore
   basename = shared.unsuffixed(original_filename)
   if '.jso' in basename:
     basename = shared.unsuffixed(basename)
-  output_file = basename + '.jso%d.js' % acorn_optimizer.counter
+  output_file = basename + '.jso%d.js' % acorn_optimizer.counter  # type: ignore
   shared.get_temp_files().note(output_file)
   cmd += ['-o', output_file]
   check_call(cmd)
@@ -1241,6 +1243,7 @@
 
 def get_binaryen_bin():
   global binaryen_checked
+  assert config.BINARYEN_ROOT
   rtn = os.path.join(config.BINARYEN_ROOT, 'bin')
   if not binaryen_checked:
     check_binaryen(rtn)
@@ -1311,6 +1314,7 @@
   if DEBUG:
     dst = 'emcc-%d-%s' % (save_intermediate.counter, dst)
     save_intermediate.counter += 1
+    assert shared.CANONICAL_TEMP_DIR
     dst = os.path.join(shared.CANONICAL_TEMP_DIR, dst)
     logger.debug('saving debug copy %s' % dst)
     shutil.copyfile(src, dst)
diff --git a/tools/config.py b/tools/config.py
index 843afa2..00935e2 100644
--- a/tools/config.py
+++ b/tools/config.py
@@ -6,7 +6,7 @@
 import os
 import sys
 import logging
-from typing import List, Optional
+from typing import List, Dict, Optional
 
 from . import utils, diagnostics
 from .utils import path_from_root, exit_with_error, __rootpath__, which
@@ -18,8 +18,8 @@
 # is in ALL_UPPER_CASE is condifered a valid config file key.
 # See parse_config_file below.
 EMSCRIPTEN_ROOT = __rootpath__
-NODE_JS = None
-BINARYEN_ROOT = None
+NODE_JS: Optional[List[str]] = None
+BINARYEN_ROOT: Optional[str] = None
 SPIDERMONKEY_ENGINE = None
 V8_ENGINE: Optional[List[str]] = None
 LLVM_ROOT = None
@@ -61,6 +61,7 @@
 
   # EM_CONFIG stuff
   if not JS_ENGINES:
+    assert NODE_JS
     JS_ENGINES = [NODE_JS]
 
   # Engine tweaks
@@ -114,7 +115,7 @@
 
   Also check EM_<KEY> environment variables to override specific config keys.
   """
-  config = {}
+  config: Dict[str, object] = {}
   config_text = utils.read_file(EM_CONFIG)
   try:
     exec(config_text, config)
diff --git a/tools/diagnostics.py b/tools/diagnostics.py
index b34fe9a..919ed4d 100644
--- a/tools/diagnostics.py
+++ b/tools/diagnostics.py
@@ -6,11 +6,11 @@
 """Simple color-enabled diagnositics reporting functions.
 """
 
-import ctypes
+import ctypes  # type: ignore
 import logging
 import os
 import sys
-from typing import Dict
+from typing import Dict, NoReturn
 
 
 WINDOWS = sys.platform.startswith('win')
@@ -147,7 +147,7 @@
   sys.stderr.write('\n')
 
 
-def error(msg, *args):
+def error(msg, *args) -> NoReturn:
   diag(ERROR, msg, *args)
   sys.exit(1)
 
diff --git a/tools/filelock.py b/tools/filelock.py
index 9f6637e..b8ef7ce 100644
--- a/tools/filelock.py
+++ b/tools/filelock.py
@@ -37,24 +37,23 @@
 
 # Modules
 # ------------------------------------------------
+import fcntl
 import logging
 import os
 import threading
 import time
+import sys
+from typing import Optional, Type
 try:
     import warnings
 except ImportError:
-    warnings = None
+    pass
 
 try:
     import msvcrt
 except ImportError:
-    msvcrt = None
+    pass
 
-try:
-    import fcntl
-except ImportError:
-    fcntl = None
 
 
 # Backward compatibility
@@ -72,7 +71,6 @@
     "BaseFileLock",
     "WindowsFileLock",
     "UnixFileLock",
-    "SoftFileLock",
     "FileLock"
 ]
 
@@ -409,50 +407,8 @@
         os.close(fd)
         return None
 
-# Soft lock
-# ~~~~~~~~~
 
-class SoftFileLock(BaseFileLock):
-    """
-    Simply watches the existence of the lock file.
-    """
-
-    def _acquire(self):
-        open_mode = os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_TRUNC
-        try:
-            fd = os.open(self._lock_file, open_mode)
-        except (IOError, OSError):
-            pass
-        else:
-            self._lock_file_fd = fd
-        return None
-
-    def _release(self):
-        os.close(self._lock_file_fd)
-        self._lock_file_fd = None
-
-        try:
-            os.remove(self._lock_file)
-        # The file is already deleted and that's what we want.
-        except OSError:
-            pass
-        return None
-
-
-# Platform filelock
-# ~~~~~~~~~~~~~~~~~
-
-#: Alias for the lock, which should be used for the current platform. On
-#: Windows, this is an alias for :class:`WindowsFileLock`, on Unix for
-#: :class:`UnixFileLock` and otherwise for :class:`SoftFileLock`.
-FileLock = None
-
-if msvcrt:
+if sys.platform == "win32":
     FileLock = WindowsFileLock
-elif fcntl:
-    FileLock = UnixFileLock
 else:
-    FileLock = SoftFileLock
-
-    if warnings is not None:
-        warnings.warn("only soft file lock is available")
+    FileLock = UnixFileLock
diff --git a/tools/js_optimizer.py b/tools/js_optimizer.py
index e636198..02fc95e 100755
--- a/tools/js_optimizer.py
+++ b/tools/js_optimizer.py
@@ -390,7 +390,6 @@
   return filename
 
 
-@ToolchainProfiler.profile_block('js_optimizer.run_on_js')
 def run(filename, passes, extra_info=None):
   just_split = 'receiveJSON' in passes
   just_concat = 'emitJSON' in passes
diff --git a/tools/shared.py b/tools/shared.py
index 0810b83..cfec847 100644
--- a/tools/shared.py
+++ b/tools/shared.py
@@ -19,6 +19,7 @@
 import stat
 import sys
 import tempfile
+from typing import Optional, List, Dict, Tuple, Callable, Any
 
 # We depend on python 3.6 for fstring support
 if sys.version_info < (3, 6):
@@ -84,7 +85,7 @@
 
 
 # TODO(sbc): Investigate switching to shlex.quote
-def shlex_quote(arg):
+def shlex_quote(arg: str) -> str:
   arg = os.fspath(arg)
   if ' ' in arg and (not (arg.startswith('"') and arg.endswith('"'))) and (not (arg.startswith("'") and arg.endswith("'"))):
     return '"' + arg.replace('"', '\\"') + '"'
@@ -94,11 +95,11 @@
 
 # Switch to shlex.join once we can depend on python 3.8:
 # https://docs.python.org/3/library/shlex.html#shlex.join
-def shlex_join(cmd):
+def shlex_join(cmd: List[str]) -> str:
   return ' '.join(shlex_quote(x) for x in cmd)
 
 
-def run_process(cmd, check=True, input=None, *args, **kw):
+def run_process(cmd: List[str], check: bool = True, input: str = None, *args: Any, **kw: Any) -> subprocess.CompletedProcess[Any]:
   """Runs a subprocess returning the exit code.
 
   By default this function will raise an exception on failure.  Therefor this should only be
@@ -118,21 +119,24 @@
   return ret
 
 
-def get_num_cores():
-  return int(os.environ.get('EMCC_CORES', os.cpu_count()))
+def get_num_cores() -> int:
+  if 'EMCC_CORES' in os.environ:
+    return int(os.environ['EMCC_CORES'])
+  return os.cpu_count() or 4
 
 
-def mp_run_process(command_tuple):
+def mp_run_process(command_tuple: Tuple[List[str], Dict[str, str], Optional[str]]) -> str:
   cmd, env, route_stdout_to_temp_files_suffix = command_tuple
   stdout = None
   if route_stdout_to_temp_files_suffix:
     stdout = get_temp_files().get(route_stdout_to_temp_files_suffix)
   subprocess.run(cmd, stdout=stdout, stderr=None, env=env, check=True)
-  if route_stdout_to_temp_files_suffix:
+  if stdout:
     return stdout.name
+  return ''
 
 
-def returncode_to_str(code):
+def returncode_to_str(code: int) -> str:
   assert code != 0
   if code < 0:
     signal_name = signal.Signals(-code).name
@@ -141,9 +145,9 @@
   return f'returned {code}'
 
 
-def run_multiple_processes(commands,
-                           env=None,
-                           route_stdout_to_temp_files_suffix=None):
+def run_multiple_processes(commands: List[List[str]],
+                           env: Dict[str, str] = None,
+                           route_stdout_to_temp_files_suffix: Optional[str] = None) -> Optional[List[str]]:
   """Runs multiple subprocess commands.
 
   route_stdout_to_temp_files_suffix : string
@@ -180,9 +184,9 @@
 
   # Map containing all currently running processes.
   # command index -> proc/Popen object
-  processes = {}
+  processes: Dict[int, subprocess.Popen] = {}
 
-  def get_finished_process():
+  def get_finished_process() -> int:
     while True:
       for idx, proc in processes.items():
         if proc.poll() is not None:
@@ -229,8 +233,10 @@
     std_outs.sort(key=lambda x: x[0])
     return [x[1] for x in std_outs]
 
+  return None
 
-def check_call(cmd, *args, **kw):
+
+def check_call(cmd: List[str], *args: Any, **kw: Any) -> subprocess.CompletedProcess:
   """Like `run_process` above but treat failures as fatal and exit_with_error."""
   print_compiler_stage(cmd)
   try:
@@ -241,21 +247,23 @@
     exit_with_error("'%s' failed: %s", shlex_join(cmd), str(e))
 
 
-def run_js_tool(filename, jsargs=[], node_args=[], **kw):  # noqa: mutable default args
+def run_js_tool(filename: str, jsargs: List[str] = [], node_args: List[str] = [], **kw: Any) -> str:  # noqa: mutable default args
   """Execute a javascript tool.
 
   This is used by emcc to run parts of the build process that are written
   implemented in javascript.
   """
   check_node()
+  assert config.NODE_JS
   command = config.NODE_JS + node_args + [filename] + jsargs
   return check_call(command, **kw).stdout
 
 
-def get_npm_cmd(name):
+def get_npm_cmd(name: str) -> List[str]:
   if WINDOWS:
     cmd = [path_from_root('node_modules/.bin', name + '.cmd')]
   else:
+    assert config.NODE_JS
     cmd = config.NODE_JS + [path_from_root('node_modules/.bin', name)]
   if not os.path.exists(cmd[-1]):
     exit_with_error(f'{name} was not found! Please run "npm install" in Emscripten root directory to set up npm dependencies')
@@ -263,12 +271,12 @@
 
 
 # TODO(sbc): Replace with functools.cache, once we update to python 3.7
-def memoize(func):
+def memoize(func: Callable) -> Callable:
   called = False
   result = None
 
   @wraps(func)
-  def helper():
+  def helper() -> Any:
     nonlocal called, result
     if not called:
       result = func()
@@ -279,15 +287,17 @@
 
 
 @memoize
-def get_clang_version():
+def get_clang_version() -> str:
   if not os.path.exists(CLANG_CC):
     exit_with_error('clang executable not found at `%s`' % CLANG_CC)
   proc = check_call([CLANG_CC, '--version'], stdout=PIPE)
   m = re.search(r'[Vv]ersion\s+(\d+\.\d+)', proc.stdout)
-  return m and m.group(1)
+  if not m:
+    return ''
+  return m.group(1)
 
 
-def check_llvm_version():
+def check_llvm_version() -> bool:
   actual = get_clang_version()
   if EXPECTED_LLVM_VERSION in actual:
     return True
@@ -295,19 +305,19 @@
   return False
 
 
-def get_clang_targets():
+def get_clang_targets() -> str:
   if not os.path.exists(CLANG_CC):
     exit_with_error('clang executable not found at `%s`' % CLANG_CC)
   try:
     target_info = run_process([CLANG_CC, '-print-targets'], stdout=PIPE).stdout
-  except subprocess.CalldProcessError:
+  except subprocess.CalledProcessError:
     exit_with_error('error running `clang -print-targets`.  Check your llvm installation (%s)' % CLANG_CC)
   if 'Registered Targets:' not in target_info:
     exit_with_error('error parsing output of `clang -print-targets`.  Check your llvm installation (%s)' % CLANG_CC)
   return target_info.split('Registered Targets:')[1]
 
 
-def check_llvm():
+def check_llvm() -> bool:
   targets = get_clang_targets()
   if 'wasm32' not in targets:
     logger.critical('LLVM has not been built with the WebAssembly backend, clang reports:')
@@ -319,22 +329,24 @@
   return True
 
 
-def get_node_directory():
-  return os.path.dirname(config.NODE_JS[0] if type(config.NODE_JS) is list else config.NODE_JS)
+def get_node_directory() -> str:
+  assert config.NODE_JS
+  return os.path.dirname(config.NODE_JS[0])
 
 
 # When we run some tools from npm (closure, html-minifier-terser), those
 # expect that the tools have node.js accessible in PATH. Place our node
 # there when invoking those tools.
-def env_with_node_in_path():
+def env_with_node_in_path() -> Dict[str, str]:
   env = os.environ.copy()
   env['PATH'] = get_node_directory() + os.pathsep + env['PATH']
   return env
 
 
 @memoize
-def check_node_version():
+def check_node_version() -> None:
   try:
+    assert config.NODE_JS
     actual = run_process(config.NODE_JS + ['--version'], stdout=PIPE).stdout.strip()
     version = actual.replace('v', '')
     version = version.split('-')[0].split('.')
@@ -350,7 +362,7 @@
   return version
 
 
-def node_bigint_flags():
+def node_bigint_flags() -> List[str]:
   node_version = check_node_version()
   # wasm bigint was enabled by default in node v16.
   if node_version and node_version < (16, 0, 0):
@@ -359,7 +371,7 @@
     return []
 
 
-def node_pthread_flags():
+def node_pthread_flags() -> List[str]:
   node_version = check_node_version()
   # bulk memory and wasm threads were enabled by default in node v16.
   if node_version and node_version < (16, 0, 0):
@@ -370,15 +382,16 @@
 
 @memoize
 @ToolchainProfiler.profile()
-def check_node():
+def check_node() -> None:
   check_node_version()
   try:
+    assert config.NODE_JS
     run_process(config.NODE_JS + ['-e', 'console.log("hello")'], stdout=PIPE)
   except Exception as e:
     exit_with_error('The configured node executable (%s) does not seem to work, check the paths in %s (%s)', config.NODE_JS, config.EM_CONFIG, str(e))
 
 
-def set_version_globals():
+def set_version_globals() -> None:
   global EMSCRIPTEN_VERSION, EMSCRIPTEN_VERSION_MAJOR, EMSCRIPTEN_VERSION_MINOR, EMSCRIPTEN_VERSION_TINY
   filename = path_from_root('emscripten-version.txt')
   EMSCRIPTEN_VERSION = utils.read_file(filename).strip().strip('"')
@@ -386,7 +399,7 @@
   EMSCRIPTEN_VERSION_MAJOR, EMSCRIPTEN_VERSION_MINOR, EMSCRIPTEN_VERSION_TINY = parts
 
 
-def generate_sanity():
+def generate_sanity() -> str:
   sanity_file_content = f'{EMSCRIPTEN_VERSION}|{config.LLVM_ROOT}|{get_clang_version()}'
   if os.path.exists(config.EM_CONFIG):
     config_data = utils.read_file(config.EM_CONFIG)
@@ -397,7 +410,7 @@
   return sanity_file_content
 
 
-def perform_sanity_checks():
+def perform_sanity_checks() -> None:
   # some warning, mostly not fatal checks - do them even if EM_IGNORE_SANITY is on
   check_llvm_version()
 
@@ -419,7 +432,7 @@
 
 
 @ToolchainProfiler.profile()
-def check_sanity(force=False):
+def check_sanity(force: bool = False) -> None:
   """Check that basic stuff we need (a JS engine to compile, Node.js, and Clang
   and LLVM) exists.
 
@@ -453,7 +466,7 @@
 
   sanity_file = cache.get_path('sanity.txt')
 
-  def sanity_is_correct():
+  def sanity_is_correct() -> bool:
     if os.path.exists(sanity_file):
       sanity_data = utils.read_file(sanity_file)
       if sanity_data == expected:
@@ -495,7 +508,8 @@
 
 # Some distributions ship with multiple llvm versions so they add
 # the version to the binaries, cope with that
-def build_llvm_tool_path(tool):
+def build_llvm_tool_path(tool: str) -> str:
+  assert config.LLVM_ROOT
   if config.LLVM_ADD_VERSION:
     return os.path.join(config.LLVM_ROOT, tool + "-" + config.LLVM_ADD_VERSION)
   else:
@@ -504,22 +518,23 @@
 
 # Some distributions ship with multiple clang versions so they add
 # the version to the binaries, cope with that
-def build_clang_tool_path(tool):
+def build_clang_tool_path(tool: str) -> str:
+  assert config.LLVM_ROOT
   if config.CLANG_ADD_VERSION:
     return os.path.join(config.LLVM_ROOT, tool + "-" + config.CLANG_ADD_VERSION)
   else:
     return os.path.join(config.LLVM_ROOT, tool)
 
 
-def exe_suffix(cmd):
+def exe_suffix(cmd: str) -> str:
   return cmd + '.exe' if WINDOWS else cmd
 
 
-def bat_suffix(cmd):
+def bat_suffix(cmd: str) -> str:
   return cmd + '.bat' if WINDOWS else cmd
 
 
-def replace_suffix(filename, new_suffix):
+def replace_suffix(filename: str, new_suffix: str) -> str:
   assert new_suffix[0] == '.'
   return os.path.splitext(filename)[0] + new_suffix
 
@@ -527,7 +542,7 @@
 # In MINIMAL_RUNTIME mode, keep suffixes of generated files simple
 # ('.mem' instead of '.js.mem'; .'symbols' instead of '.js.symbols' etc)
 # Retain the original naming scheme in traditional runtime.
-def replace_or_append_suffix(filename, new_suffix):
+def replace_or_append_suffix(filename: str, new_suffix: str) -> str:
   assert new_suffix[0] == '.'
   return replace_suffix(filename, new_suffix) if settings.MINIMAL_RUNTIME else filename + new_suffix
 
@@ -535,15 +550,16 @@
 # Temp dir. Create a random one, unless EMCC_DEBUG is set, in which case use the canonical
 # temp directory (TEMP_DIR/emscripten_temp).
 @memoize
-def get_emscripten_temp_dir():
+def get_emscripten_temp_dir() -> str:
   """Returns a path to EMSCRIPTEN_TEMP_DIR, creating one if it didn't exist."""
   global EMSCRIPTEN_TEMP_DIR
+  assert TEMP_DIR
   if not EMSCRIPTEN_TEMP_DIR:
     EMSCRIPTEN_TEMP_DIR = tempfile.mkdtemp(prefix='emscripten_temp_', dir=TEMP_DIR)
 
     if not DEBUG_SAVE:
-      def prepare_to_clean_temp(d):
-        def clean_temp():
+      def prepare_to_clean_temp(d: str) -> None:
+        def clean_temp() -> None:
           utils.delete_dir(d)
 
         atexit.register(clean_temp)
@@ -552,19 +568,21 @@
   return EMSCRIPTEN_TEMP_DIR
 
 
-def get_canonical_temp_dir(temp_dir):
+def get_canonical_temp_dir(temp_dir: str) -> str:
   return os.path.join(temp_dir, 'emscripten_temp')
 
 
-def setup_temp_dirs():
+def setup_temp_dirs() -> None:
   global EMSCRIPTEN_TEMP_DIR, CANONICAL_TEMP_DIR, TEMP_DIR
   EMSCRIPTEN_TEMP_DIR = None
 
   TEMP_DIR = os.environ.get("EMCC_TEMP_DIR", tempfile.gettempdir())
+  assert TEMP_DIR
   if not os.path.isdir(TEMP_DIR):
     exit_with_error(f'The temporary directory `{TEMP_DIR}` does not exist! Please make sure that the path is correct.')
 
   CANONICAL_TEMP_DIR = get_canonical_temp_dir(TEMP_DIR)
+  assert CANONICAL_TEMP_DIR
 
   if DEBUG:
     EMSCRIPTEN_TEMP_DIR = CANONICAL_TEMP_DIR
@@ -582,6 +600,7 @@
     # though, since emcc can recursively call itself when building
     # libraries and ports.
     if 'EM_HAVE_TEMP_DIR_LOCK' not in os.environ:
+      assert EMSCRIPTEN_TEMP_DIR
       filelock_name = os.path.join(EMSCRIPTEN_TEMP_DIR, 'emscripten.lock')
       lock = filelock.FileLock(filelock_name)
       os.environ['EM_HAVE_TEMP_DIR_LOCK'] = '1'
@@ -590,7 +609,7 @@
 
 
 @memoize
-def get_temp_files():
+def get_temp_files() -> tempfiles.TempFiles:
   if DEBUG_SAVE:
     # In debug mode store all temp files in the emscripten-specific temp dir
     # and don't worry about cleaning them up.
@@ -600,11 +619,11 @@
     return tempfiles.TempFiles(TEMP_DIR, save_debug_files=False)
 
 
-def target_environment_may_be(environment):
+def target_environment_may_be(environment: str) -> bool:
   return not settings.ENVIRONMENT or environment in settings.ENVIRONMENT.split(',')
 
 
-def print_compiler_stage(cmd):
+def print_compiler_stage(cmd: List[str]) -> None:
   """Emulate the '-v' of clang/gcc by printing the name of the sub-command
   before executing it."""
   if PRINT_STAGES:
@@ -612,21 +631,21 @@
     sys.stderr.flush()
 
 
-def mangle_c_symbol_name(name):
+def mangle_c_symbol_name(name: str) -> str:
   return '_' + name if not name.startswith('$') else name[1:]
 
 
-def demangle_c_symbol_name(name):
+def demangle_c_symbol_name(name: str) -> str:
   if not is_c_symbol(name):
     return '$' + name
   return name[1:] if name.startswith('_') else name
 
 
-def is_c_symbol(name):
+def is_c_symbol(name: str) -> bool:
   return name.startswith('_') or name in settings.WASM_SYSTEM_EXPORTS
 
 
-def treat_as_user_function(name):
+def treat_as_user_function(name: str) -> bool:
   if name.startswith('dynCall_'):
     return False
   if name in settings.WASM_SYSTEM_EXPORTS:
@@ -634,7 +653,7 @@
   return True
 
 
-def asmjs_mangle(name):
+def asmjs_mangle(name: str) -> str:
   """Mangle a name the way asm.js/JSBackend globals are mangled.
 
   Prepends '_' and replaces non-alphanumerics with '_'.
@@ -649,12 +668,12 @@
   return name
 
 
-def suffix(name):
+def suffix(name: str) -> str:
   """Return the file extension"""
   return os.path.splitext(name)[1]
 
 
-def unsuffixed(name):
+def unsuffixed(name: str) -> str:
   """Return the filename without the extension.
 
   If there are multiple extensions this strips only the final one.
@@ -662,22 +681,22 @@
   return os.path.splitext(name)[0]
 
 
-def unsuffixed_basename(name):
+def unsuffixed_basename(name: str) -> str:
   return os.path.basename(unsuffixed(name))
 
 
-def strip_prefix(string, prefix):
+def strip_prefix(string: str, prefix: str) -> str:
   assert string.startswith(prefix)
   return string[len(prefix):]
 
 
-def make_writable(filename):
+def make_writable(filename: str) -> None:
   assert os.path.isfile(filename)
   old_mode = stat.S_IMODE(os.stat(filename).st_mode)
   os.chmod(filename, old_mode | stat.S_IWUSR)
 
 
-def safe_copy(src, dst):
+def safe_copy(src: str, dst: str) -> None:
   logging.debug('copy: %s -> %s', src, dst)
   src = os.path.abspath(src)
   dst = os.path.abspath(dst)
@@ -694,7 +713,7 @@
   make_writable(dst)
 
 
-def read_and_preprocess(filename, expand_macros=False):
+def read_and_preprocess(filename: str, expand_macros: bool = False) -> str:
   temp_dir = get_emscripten_temp_dir()
   # Create a settings file with the current settings to pass to the JS preprocessor
 
@@ -711,6 +730,7 @@
   # N.B. We can't use the default stdout=PIPE here as it only allows 64K of output before it hangs
   # and shell.html is bigger than that!
   # See https://thraxil.org/users/anders/posts/2008/03/13/Subprocess-Hanging-PIPE-is-your-enemy/
+  dirname: Optional[str]
   dirname, filename = os.path.split(filename)
   if not dirname:
     dirname = None
@@ -725,13 +745,13 @@
   return out
 
 
-def do_replace(input_, pattern, replacement):
+def do_replace(input_: str, pattern: str, replacement: str) -> str:
   if pattern not in input_:
     exit_with_error('expected to find pattern in input JS: %s' % pattern)
   return input_.replace(pattern, replacement)
 
 
-def get_llvm_target():
+def get_llvm_target() -> str:
   if settings.MEMORY64:
     return 'wasm64-unknown-emscripten'
   else:
@@ -746,8 +766,6 @@
 # file.  TODO(sbc): We should try to reduce that amount we do here and instead
 # have consumers explicitly call initialization functions.
 
-set_version_globals()
-
 CLANG_CC = os.path.expanduser(build_clang_tool_path(exe_suffix('clang')))
 CLANG_CXX = os.path.expanduser(build_clang_tool_path(exe_suffix('clang++')))
 LLVM_AR = build_llvm_tool_path(exe_suffix('llvm-ar'))
@@ -769,12 +787,17 @@
 FILE_PACKAGER = bat_suffix(path_from_root('tools/file_packager'))
 WASM_SOURCEMAP = bat_suffix(path_from_root('tools/wasm-sourcemap'))
 
-# These get set by setup_temp_dirs
-TEMP_DIR = None
-EMSCRIPTEN_TEMP_DIR = None
+# Globals that get assigned later on (by e.g. set_version_globals and setup_temp_dirs)
+TEMP_DIR: Optional[str] = None
+EMSCRIPTEN_TEMP_DIR: Optional[str] = None
+EMSCRIPTEN_VERSION = None
+EMSCRIPTEN_VERSION_MAJOR = None
+EMSCRIPTEN_VERSION_MINOR = None
+EMSCRIPTEN_VERSION_TINY = None
+CANONICAL_TEMP_DIR: Optional[str] = None
 
+set_version_globals()
 setup_temp_dirs()
-
 cache.setup(config.CACHE)
 
 PRINT_STAGES = int(os.getenv('EMCC_VERBOSE', '0'))
diff --git a/tools/system_libs.py b/tools/system_libs.py
index 02e0570..a166766 100644
--- a/tools/system_libs.py
+++ b/tools/system_libs.py
@@ -12,7 +12,7 @@
 import textwrap
 from enum import IntEnum, auto
 from glob import iglob
-from typing import List, Optional
+from typing import List, Tuple, Dict, Optional
 
 from . import shared, building, utils
 from . import deps_info
@@ -206,8 +206,8 @@
       out += '\n'
 
     objects = sorted(objects, key=lambda x: os.path.basename(x))
-    objects = ' '.join(objects)
-    out += f'build {libname}: archive {objects}\n'
+    objects_str = ' '.join(objects)
+    out += f'build {libname}: archive {objects_str}\n'
 
   utils.write_file(filename, out)
 
@@ -322,6 +322,8 @@
   # Whether to always generate WASM object files, even when LTO is set
   force_object_files = False
 
+  useable_variations: Optional[Dict[str, 'Library']] = None
+
   def __init__(self):
     """
     Creates a variation of this library.
@@ -612,7 +614,7 @@
 
     This returns a dictionary of simple names to Library objects.
     """
-    if not hasattr(cls, 'useable_variations'):
+    if cls.useable_variations is None:
       cls.useable_variations = {}
       for subclass in cls.get_inheritance_tree():
         if subclass.name:
@@ -1049,15 +1051,15 @@
       'pow_small.c', 'log_small.c', 'log2_small.c'
     ]
 
-    ignore = set(ignore)
+    ignore_set = set(ignore)
     for dirpath, dirnames, filenames in os.walk(musl_srcdir):
       # Don't recurse into ingored directories
-      remove = [d for d in dirnames if d in ignore]
+      remove = [d for d in dirnames if d in ignore_set]
       for r in remove:
         dirnames.remove(r)
 
       for f in filenames:
-        if f.endswith('.c') and f not in ignore:
+        if f.endswith('.c') and f not in ignore_set:
           libc_files.append(os.path.join(musl_srcdir, dirpath, f))
 
     # Allowed files from ignored modules
@@ -1993,7 +1995,7 @@
 
 
 def get_libs_to_link(args, forced, only_forced):
-  libs_to_link = []
+  libs_to_link: List[Tuple[str, bool]] = []
 
   if '-nostdlib' in args:
     return libs_to_link
diff --git a/tools/utils.py b/tools/utils.py
index 06f8ac9..4666919 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -8,6 +8,7 @@
 import shutil
 import sys
 from pathlib import Path
+from typing import NoReturn, List
 
 from . import diagnostics
 
@@ -17,7 +18,7 @@
 LINUX = sys.platform.startswith('linux')
 
 
-def exit_with_error(msg, *args):
+def exit_with_error(msg, *args) -> NoReturn:
   diagnostics.error(msg, *args)
 
 
@@ -110,7 +111,7 @@
   shutil.rmtree(dirname)
 
 
-def delete_contents(dirname, exclude=None):
+def delete_contents(dirname: str, exclude: List[str] = None) -> None:
   """Delete the contents of a directory without removing
   the directory itself."""
   if not os.path.exists(dirname):