DEV Community

Cover image for I'm Porting Node.js 22 to a 20-Year-Old Power Mac G5. It's Going About as Well as You'd Expect.
AutoJanitor
AutoJanitor

Posted on

I'm Porting Node.js 22 to a 20-Year-Old Power Mac G5. It's Going About as Well as You'd Expect.

The Machine

Somewhere in my lab in Louisiana, a Power Mac G5 Dual sits on a shelf. Dual 2.0 GHz PowerPC 970 processors, 8 GB of RAM, running Mac OS X Leopard 10.5. It was the fastest Mac you could buy in 2005. Apple called it "the world's fastest personal computer." Then they switched to Intel and never looked back.

Twenty years later, I'm trying to build Node.js 22 on it.

Why Would Anyone Do This?

Two reasons.

First: I run a blockchain called RustChain that uses Proof-of-Antiquity consensus. Vintage hardware earns higher mining rewards. The G5 gets a 2.0x antiquity multiplier on its RTC token earnings. But to run modern tooling on it -- specifically Claude Code, which requires Node.js -- I need a working Node runtime.

Second: Because it's there. The G5 is a beautiful piece of engineering. Dual 64-bit PowerPC cores, big-endian byte order, AltiVec SIMD. It represents a road not taken in computing history. If we want an agent internet that runs everywhere, "everywhere" should include hardware like this.

Third (okay, three reasons): I already run LLMs on a 768GB IBM POWER8 server. Once you've gone down the PowerPC rabbit hole, a G5 Node.js build seems almost reasonable.

The Setup

Spec Value
Machine Power Mac G5 Dual
CPU 2x PowerPC 970 @ 2.0 GHz
RAM 8 GB
OS Mac OS X Leopard 10.5 (Darwin 9.8.0)
Compiler GCC 10.5.0 (cross-compiled, lives at /usr/local/gcc-10/bin/gcc)
Target Node.js v22
Byte Order Big Endian

The system compiler on Leopard is GCC 4.0. Node.js 22 requires C++20. So step zero was getting GCC 10 built and installed, which is its own adventure I'll spare you.

SSH requires legacy crypto flags because Leopard's OpenSSH is ancient:

ssh -o HostKeyAlgorithms=+ssh-rsa \
    -o PubkeyAcceptedAlgorithms=+ssh-rsa \
    selenamac@192.168.0.179
Enter fullscreen mode Exit fullscreen mode

Patch 1: C++20 for js2c.cc

Node's js2c.cc tool and the Ada URL parser use C++20 string methods like starts_with() and ends_with(). The configure script doesn't propagate -std=gnu++20 to all compilation targets.

Fix: Patch all 85+ generated makefiles:

# Python script to inject -std=gnu++20 into every makefile
import glob
for mk in glob.glob('out/**/*.mk', recursive=True):
    content = open(mk).read()
    if 'CFLAGS_CC_Release' in content and '-std=gnu++20' not in content:
        content = content.replace(
            "CFLAGS_CC_Release =",
            "CFLAGS_CC_Release = -std=gnu++20"
        )
        open(mk, 'w').write(content)
Enter fullscreen mode Exit fullscreen mode

This is the first patch. There will be nine more.

Patch 2: GCC 10's C++20 Identity Crisis

GCC 10 with -std=gnu++20 reports __cplusplus = 201709L (C++17). But it actually provides C++20 library features like <bit> and std::endian. Node's src/util.h has fallback code guarded by __cplusplus < 202002L that conflicts with GCC's actual C++20 library.

// src/util.h -- before fix
#if __cplusplus < 202002L || !defined(__cpp_lib_endian)
// Fallback endian implementation that clashes with <bit>
#endif
Enter fullscreen mode Exit fullscreen mode

Fix: Use feature-test macros instead of __cplusplus version:

#include <version>
#include <bit>

#ifndef __cpp_lib_endian
// Only use fallback if the feature truly isn't available
#endif
Enter fullscreen mode Exit fullscreen mode

Patch 3: char8_t Cast

A reinterpret_cast<const char*>(out()) in util.h needs to be const char8_t* when C++20's char8_t is enabled. One-line fix. Moving on.

Patch 4: ncrypto.cc constexpr

// deps/ncrypto/ncrypto.cc line 1692
// GCC 10 is stricter about uninitialized variables in constexpr-adjacent contexts
size_t offset = 0, len = 0;  // was: size_t offset, len;
Enter fullscreen mode Exit fullscreen mode

Patch 5: libatomic for OpenSSL

OpenSSL uses 64-bit atomic operations that aren't available in the G5's default runtime libraries. The linker throws a wall of undefined reference to __atomic_* errors.

Fix: Add -L/usr/local/gcc-10/lib -latomic to the OpenSSL and node target makefiles:

# out/deps/openssl/openssl.target.mk
LIBS := ... -L/usr/local/gcc-10/lib -latomic
Enter fullscreen mode Exit fullscreen mode

Four makefiles need this: openssl-cli, openssl-fipsmodule, openssl, and node.

Patch 6: OpenSSL Big Endian

OpenSSL needs to know it's running big-endian. Created gypi configuration files with B_ENDIAN defined. Standard stuff for any BE port.

Patch 7: V8 Thinks PPC = 32-bit

This is where things get interesting.

V8 has architecture defines: V8_TARGET_ARCH_PPC for 32-bit PowerPC and V8_TARGET_ARCH_PPC64 for 64-bit. Node's configure script detects the G5 as ppc (not ppc64), so it sets V8_TARGET_ARCH_PPC.

But V8's compiler/c-linkage.cc only defines CALLEE_SAVE_REGISTERS for PPC64:

#elif V8_TARGET_ARCH_PPC64
constexpr RegList kCalleeSaveRegisters = {
    r14, r15, r16, r17, r18, r19, r20, r21, ...
};
Enter fullscreen mode Exit fullscreen mode

No PPC case exists. Compilation fails.

Fix: Two changes. First, replace V8_TARGET_ARCH_PPC with V8_TARGET_ARCH_PPC64 in all makefiles:

find out -name '*.mk' -exec sed -i '' \
  's/-DV8_TARGET_ARCH_PPC -DV8_TARGET_ARCH_PPC64/-DV8_TARGET_ARCH_PPC64/g' {} \;
Enter fullscreen mode Exit fullscreen mode

Second, patch V8 source to accept either:

// deps/v8/src/compiler/c-linkage.cc line 88
#elif V8_TARGET_ARCH_PPC64 || V8_TARGET_ARCH_PPC

// deps/v8/src/compiler/pipeline.cc (4 locations)
defined(V8_TARGET_ARCH_PPC64) || defined(V8_TARGET_ARCH_PPC)
Enter fullscreen mode Exit fullscreen mode

Patch 8: The 64-bit Revelation

After all those fixes, V8 hits a static assertion in globals.h:

static_assert((kTaggedSize == 8) == TAGGED_SIZE_8_BYTES);
Enter fullscreen mode Exit fullscreen mode

kTaggedSize is 8 (because we told V8 it's PPC64), but sizeof(void*) is 4 because GCC is compiling in 32-bit mode by default. The G5 is a 64-bit CPU, but Darwin's default ABI is 32-bit.

Fix: Force 64-bit compilation everywhere:

CC='/usr/local/gcc-10/bin/gcc -m64' \
CXX='/usr/local/gcc-10/bin/g++ -m64' \
CFLAGS='-m64' CXXFLAGS='-m64' LDFLAGS='-m64' \
./configure --dest-cpu=ppc64 --openssl-no-asm \
            --without-intl --without-inspector
Enter fullscreen mode Exit fullscreen mode

This means a full rebuild. Every object file from the 32-bit build is now wrong.

The configure script also injects -arch i386 into makefiles (a bizarre default for a PPC machine), so those need to be patched out too:

find out -name '*.mk' -exec sed -i '' 's/-arch/-m64 #-arch/g' {} \;
find out -name '*.mk' -exec sed -i '' 's/^  i386/   #i386/g' {} \;
Enter fullscreen mode Exit fullscreen mode

Patch 9: Two libstdc++ Libraries, One Problem

Here's the cruel twist: GCC 10.5.0 on this Mac was compiled as a 32-bit compiler. Its libstdc++.a is 32-bit only. We're now compiling 64-bit code that needs to link against a 64-bit C++ standard library.

But wait -- the system libstdc++ (/usr/lib/libstdc++.6.dylib) is a universal binary that includes ppc64:

$ file /usr/lib/libstdc++.6.dylib
/usr/lib/libstdc++.6.dylib: Mach-O universal binary with 4 architectures
# Includes: ppc, ppc64, i386, x86_64
Enter fullscreen mode Exit fullscreen mode

Fix: Point the linker at the system library instead of GCC's:

# Changed from:
LIBS := -L/usr/local/gcc-10/lib -lstdc++ -lm
# To:
LIBS := -L/usr/lib -lstdc++.6 -lm
Enter fullscreen mode Exit fullscreen mode

Patch 10: The Missing Symbol

The system libstdc++ is from 2007. It doesn't have __ZSt25__throw_bad_function_callv -- a C++11 symbol that std::function needs when you call an empty function object.

Fix: Write a compatibility shim:

// stdc++_compat.cpp
#include <cstdlib>
namespace std {
    void __throw_bad_function_call() { abort(); }
}
Enter fullscreen mode Exit fullscreen mode

Compile it 64-bit and add to the link inputs:

/usr/local/gcc-10/bin/g++ -m64 -std=gnu++20 -c stdc++_compat.cpp \
    -o out/Release/obj.target/stdc++_compat.o
Enter fullscreen mode Exit fullscreen mode

Current Status: Blocked

After all ten patches, the build compiles about 40 object files before the G5 went offline. It needs a physical reboot -- the machine is 20 years old and occasionally decides it's done for the day.

The next blocker will probably be something in V8's code generator. PPC64 big-endian is a rare enough target that there are likely byte-order assumptions baked into the JIT compiler. I expect at least three more patches before we see a working node binary.

What I've learned so far:

  1. V8 has no concept of 64-bit PPC without PPC64 defines. The G5 lives in a gap: it's a 64-bit processor that Apple's toolchain treats as 32-bit by default.

  2. Modern compilers on vintage systems create bizarre hybrid environments. GCC 10 provides C++20 features but lies about __cplusplus. It compiles 64-bit code but ships 32-bit libraries. Feature-test macros are the only reliable truth.

  3. Every fix creates the next problem. Enabling PPC64 requires 64-bit mode. 64-bit mode requires different libraries. Different libraries are missing symbols. It's fixes all the way down.

  4. The PowerPC architecture deserved better. These are elegant machines with real 64-bit SIMD, hardware-level big-endian support, and a clean ISA. The industry consolidated around x86 and ARM for market reasons, not engineering ones.

What's Next

Once the G5 comes back online:

  • Add stdc++_compat.o to the linker inputs for node_js2c target
  • Verify -m64 propagated to all compilation and link flags
  • Brace for V8 JIT compiler byte-order issues
  • If it builds: node --version on a Power Mac G5

The goal remains: run Claude Code on vintage PowerPC hardware, earning RustChain antiquity rewards while doing actual development work. An AI agent running on a machine old enough to vote.

I'll update this article when the G5 boots back up.


The Series: Building the Agent Internet

This is part of my ongoing series about building infrastructure for AI agents on unconventional hardware:


BoTTube Videos

Built by Elyan Labs in Louisiana.

Top comments (0)