Why is node-gyp complaining about missing distutils under Python 3.12?

Recently, I was working on an instance of Backstage and when reinstalling node_modules from scratch, I ran into this interesting error. I’ve removed most of the output for clarity.

$ yarn install
 YN0000: · Yarn 4.3.1
[...]
 YN0000: Link step
 YN0007: isolated-vm@npm:4.5.0 must be built because it never has been before or the last one failed
 YN0009: isolated-vm@npm:4.5.0 couldn't be built successfully (exit code 1, logs can be found here: /private/var/folders/_g/k61b_sdn0q14vn94nkln53_h0000gq/T/xfs-a558ccfc/build.log)
➤ YN0000: └ Completed in 2s 414ms
➤ YN0000: · Failed with errors in 3s 254ms

From here, checking the mentioned logs gave me this:

$ cat /private/var/folders/_g/k61b_sdn0q14vn94nkln53_h0000gq/T/xfs-a558ccfc/build.log
# This file contains the result of Yarn building a package (isolated-vm@npm:4.5.0)
# Script name: install

gyp info it worked if it ends with ok
gyp info using node-gyp@9.3.1
gyp info using node@20.12.2 | darwin | arm64
gyp info find Python using Python version 3.12.7 found at "/Users/marcus/.local/share/mise/installs/python/3.12.7/bin/python3"
gyp info spawn /Users/marcus/.local/share/mise/installs/python/3.12.7/bin/python3
gyp info spawn args [...]
Traceback (most recent call last):
  File "/Users/marcus/blah/node_modules/node-gyp/gyp/gyp_main.py", line 42, in <module>
    import gyp  # noqa: E402
    ^^^^^^^^^^
  File "/Users/marcus/blah/node_modules/node-gyp/gyp/pylib/gyp/__init__.py", line 9, in <module>
    import gyp.input
  File "/Users/marcus/blah/node_modules/node-gyp/gyp/pylib/gyp/input.py", line 19, in <module>
    from distutils.version import StrictVersion
ModuleNotFoundError: No module named 'distutils'
gyp ERR! configure error
gyp ERR! stack Error: `gyp` failed with exit code: 1
gyp ERR! stack     at ChildProcess.onCpExit (/Users/marcus/blah/node_modules/node-gyp/lib/configure.js:325:16)
gyp ERR! stack     at ChildProcess.emit (node:events:518:28)
gyp ERR! stack     at ChildProcess._handle.onexit (node:internal/child_process:294:12)
gyp ERR! System Darwin 24.1.0
gyp ERR! command "/Users/marcus/.local/share/mise/installs/node/20.12.2/bin/node" "/Users/marcus/blah/node_modules/node-gyp/bin/node-gyp.js" "rebuild" "--release" "-j" "4"
gyp ERR! cwd /Users/marcus/blah/node_modules/isolated-vm
gyp ERR! node -v v20.12.2
gyp ERR! node-gyp -v v9.3.1
gyp ERR! not ok

For clarity, there’s the actually important part pulled out:

Traceback (most recent call last):
  File "/Users/marcus/blah/node_modules/node-gyp/gyp/gyp_main.py", line 42, in <module>
    import gyp  # noqa: E402
    ^^^^^^^^^^
  File "/Users/marcus/blah/node_modules/node-gyp/gyp/pylib/gyp/__init__.py", line 9, in <module>
    import gyp.input
  File "/Users/marcus/blah/node_modules/node-gyp/gyp/pylib/gyp/input.py", line 19, in <module>
    from distutils.version import StrictVersion
ModuleNotFoundError: No module named 'distutils'
gyp ERR! configure error
gyp ERR! stack Error: `gyp` failed with exit code: 1

What we can see here is that some part of trying to build node-gyp attempted a call to the Python module distutils and we don’t have that module installed.

At this point, I realised I had upgraded a bunch of language runtimes recently and sure enough, distutils is no longer shipped with Python by default.

This was done via PEP 632 and announced at the very top of the Python 3.12 release notes that I haven’t read.

Anyway, for myself on macOS, I did the following to get distutils all set up:

$ brew install python-setuptools
$ pip install setuptools

Both steps are required I believe but I haven’t tested to see what if you have one or the other. I do know that running pip install fixed it but some claim that brew install was the missing half for them.

Anyway, you should be back on your way to javascripting in no time.