iTranslated by AI
Building and Running V8 JavaScript Engine from Source Locally
Overview
This article covers my experience building the V8 JavaScript engine, which is used in Chrome and Node.js, from source code on my local PC. Although the V8 JavaScript engine cannot be run by itself, a standalone version called d8 is available. In this post, I tried getting d8 to a point where it actually runs on my machine.
V8
V8 is Google’s open source high-performance JavaScript and WebAssembly engine, written in C++. It is used in Chrome and in Node.js, among others. It implements ECMAScript and WebAssembly, and runs on Windows, macOS, and Linux systems that use x64, IA-32, or ARM processors. V8 can be built as a standalone application, or can be embedded into any C++ application.
Once you can run it locally, since it is an implementation of ECMAScript (the international standard for JavaScript standardized by Ecma International), you can experiment with ECMAScript itself and also use it as a WebAssembly engine.
Development Environment
MBA Apple M3 Memory 24GB
Sonoma 14.6.1
Building V8 from source code locally
First, we will download the source code to build it locally.
Downloading the source code
On macOS, you need to install Git and depot_tools.
I assume Git needs no explanation, so let's move on to installing depot_tools.
What is depot_tools?
A collection of tools that support/extend the Chromium development environment:
- git-cl
- Home for all tools for interacting with changelists
- git-footers
- Extracts meta-information expressed as footers in commit messages
- git-freeze
- Freezes all changes on a branch (both indexed and unindexed)
- git-hyper-blame
- Similar to git blame, but with the ability to ignore or skip specific commits
- git-map-branches
- Color-coded terminal display of all local git branches with their upstream hierarchy
- git-map
- Displays the history of all branches in color-coded terminal format
- git-mark-merge-base
- Manually manipulate depot_tools' merge-base markers
- git-nav-downstream
- Checks out the downstream branch of the current branch
- git-nav-upstream
- Checks out the upstream branch of the current branch
- git-new-branch
- Creates and switches to a new branch with correct tracking information
- git-rebase-update
- Updates all branches and reflects the latest changes from upstream
- git-rename-branch
- Renames a branch and correctly maintains all downstream relationships
- git-reparent-branch
- Changes the parent (upstream) of the current branch
- git-retry
- Bootstrap function to retry git commands
- git-squash-branch
- Replaces all commits in a branch with a single commit
- git-thaw
- Unfreezes all changes on a frozen branch
- git-upstream-diff
- Displays the difference between the current branch and its upstream branch
Clone the following repository:
$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
Add the directory where you performed the git clone (/path/to/depot_tools) to the beginning of your PATH. (You should set this in ~/.bashrc, ~/.zshrc, etc.)
First, run the following command to diagnose and configure your Git/Gerrit credentials:
$ git cl creds-check --global
What is Gerrit?
A free, web-based code collaboration tool. It allows software developers in a team to review source code changes via a web browser and approve or reject them. It integrates with the Git distributed version control system.
In my environment, the following was added to ~/.gitconfig:
[credential "https://android.googlesource.com"]
helper =
helper = luci
[credential "https://aomedia.googlesource.com"]
helper =
helper = luci
[credential "https://beto-core.googlesource.com"]
helper =
helper = luci
[credential "https://boringssl.googlesource.com"]
helper =
helper = luci
[credential "https://chromium.googlesource.com"]
helper =
helper = luci
[credential "https://chrome-internal.googlesource.com"]
helper =
helper = luci
[credential "https://dawn.googlesource.com"]
helper =
helper = luci
[credential "https://pdfium.googlesource.com"]
helper =
helper = luci
[credential "https://quiche.googlesource.com"]
helper =
helper = luci
[credential "https://skia.googlesource.com"]
helper =
helper = luci
[credential "https://swiftshader.googlesource.com"]
helper =
helper = luci
[credential "https://webrtc.googlesource.com"]
helper =
helper = luci
Now that we are ready, let's create a dedicated directory and retrieve the source code.
$ mkdir chromium-v8
$ cd chromium-v8
$ fetch --no-history v8
Once the download was complete, the following files were downloaded:
$ erdtree -H -. -L 2
230.4 MiB chromium-v8
230.3 MiB ├─ v8
112.9 MiB │ ├─ test
68.4 MiB │ ├─ src
37.5 MiB │ ├─ .git
7.3 MiB │ ├─ tools
1.6 MiB │ ├─ third_party
1.5 MiB │ ├─ include
276.0 KiB │ ├─ BUILD.gn
184.0 KiB │ ├─ infra
180.0 KiB │ ├─ BUILD.bazel
76.0 KiB │ ├─ testing
64.0 KiB │ ├─ samples
60.0 KiB │ ├─ bazel
44.0 KiB │ ├─ gni
36.0 KiB │ ├─ DEPS
24.0 KiB │ ├─ PRESUBMIT.py
16.0 KiB │ ├─ build_overrides
16.0 KiB │ ├─ AUTHORS
12.0 KiB │ ├─ custom_deps
8.0 KiB │ ├─ .ycm_extra_conf.py
8.0 KiB │ ├─ WATCHLISTS
8.0 KiB │ ├─ .git-blame-ignore-revs
8.0 KiB │ ├─ docs
4.0 KiB │ ├─ .vpython3
4.0 KiB │ ├─ LICENSE.strongtalk
4.0 KiB │ ├─ .bazelrc
4.0 KiB │ ├─ PPC_OWNERS
4.0 KiB │ ├─ .mailmap
4.0 KiB │ ├─ MODULE.bazel
4.0 KiB │ ├─ S390_OWNERS
4.0 KiB │ ├─ OWNERS
4.0 KiB │ ├─ .gitattributes
4.0 KiB │ ├─ .clang-format
4.0 KiB │ ├─ LOONG_OWNERS
4.0 KiB │ ├─ .github
4.0 KiB │ ├─ .style.yapf
4.0 KiB │ ├─ LICENSE.v8
4.0 KiB │ ├─ .gitignore
4.0 KiB │ ├─ LICENSE.fdlibm
4.0 KiB │ ├─ INTL_OWNERS
4.0 KiB │ ├─ README.md
4.0 KiB │ ├─ codereview.settings
4.0 KiB │ ├─ .editorconfig
4.0 KiB │ ├─ .clang-tidy
4.0 KiB │ ├─ ENG_REVIEW_OWNERS
4.0 KiB │ ├─ MIPS_OWNERS
4.0 KiB │ ├─ .gn
4.0 KiB │ ├─ COMMON_OWNERS
4.0 KiB │ ├─ INFRA_OWNERS
4.0 KiB │ ├─ LICENSE
4.0 KiB │ ├─ pyrightconfig.json
4.0 KiB │ ├─ RISCV_OWNERS
4.0 KiB │ ├─ .flake8
4.0 KiB │ ├─ DIR_METADATA
4.0 KiB │ └─ CODE_OF_CONDUCT.md
36.0 KiB ├─ .cipd
32.0 KiB │ ├─ pkgs
4.0 KiB │ └─ tagcache.db
8.0 KiB ├─ .gclient_entries
4.0 KiB ├─ .gclient_previous_sync_commits
4.0 KiB ├─ .gcs_entries
4.0 KiB └─ .gclient
Next, I'll update the third-party repos and run the pre-compile hooks:
$ cd v8 && gclient sync
Syncing projects: 100% (46/46), done.
Running hooks: 100% (23/23), done.
Building with GN
What is GN?
GN stands for “Generate Ninja”. It is a meta-build system developed by Google for the Chromium project — it focuses on readingBUILD.gnfiles written in a custom DSL to quickly generatebuild.ninjafiles for the actual builder, Ninja.
- Generating build files
We use a convenient script, v8/tools/dev/v8gen.py, which makes it easier to generate build files for common configurations.
You can list the available configurations with the following:
# Work under the v8 directory
$ ./tools/dev/v8gen.py list
android.arm.debug
android.arm.optdebug
android.arm.release
arm.debug
arm.optdebug
arm.release
arm64.debug
arm64.optdebug
arm64.release
arm64.release.sample
ia32.debug
ia32.optdebug
ia32.release
mips64el.debug
mips64el.optdebug
mips64el.release
ppc64.debug
ppc64.debug.sim
ppc64.optdebug
ppc64.optdebug.sim
ppc64.release
ppc64.release.sim
riscv64.debug
riscv64.debug.sim
riscv64.optdebug
riscv64.optdebug.sim
riscv64.release
riscv64.release.sim
s390x.debug
s390x.debug.sim
s390x.optdebug
s390x.optdebug.sim
s390x.release
s390x.release.sim
x64.debug
x64.optdebug
x64.release
x64.release.sample
Let's actually generate the build files for Mac's Apple Silicon.
./tools/dev/v8gen.py arm64.release
Running this creates out.gn/arm64.release.
- Building
Now that we are ready, let's build d8.
ninja -C out.gn/arm64.release d8
Once the build finishes successfully, out.gn/arm64.release/d8 will be created.
d8 can be run as a standalone application.
out.gn/arm64.release/d8
V8 version 13.9.0 (candidate)
d8> console.log('hello');
hello
undefined
d8> const array = [1,2,3,4].map((x) => x * 2);
undefined
d8> console.log(array);
2,4,6,8
undefined
Regarding d8
The source code for d8 itself is located in src/d8. I'll create a test.js to check how far d8 goes in pseudo-supporting Web APIs like setTimeout and give it a try.
function testWebAPICompatibility() {
print("Web API Compatibility in d8:");
const webAPIs = [
// Console API
{ name: 'console', test: () => typeof console !== 'undefined' && typeof console.log === 'function' },
{ name: 'console.error', test: () => typeof console.error === 'function' },
{ name: 'console.warn', test: () => typeof console.warn === 'function' },
{ name: 'console.info', test: () => typeof console.info === 'function' },
{ name: 'console.debug', test: () => typeof console.debug === 'function' },
{ name: 'console.trace', test: () => typeof console.trace === 'function' },
{ name: 'console.assert', test: () => typeof console.assert === 'function' },
// Timers (usually not available in d8)
{ name: 'setTimeout', test: () => typeof setTimeout === 'function' },
{ name: 'setInterval', test: () => typeof setInterval === 'function' },
{ name: 'clearTimeout', test: () => typeof clearTimeout === 'function' },
{ name: 'clearInterval', test: () => typeof clearInterval === 'function' },
{ name: 'setImmediate', test: () => typeof setImmediate === 'function' },
// URL API
{ name: 'URL', test: () => typeof URL === 'function' },
{ name: 'URLSearchParams', test: () => typeof URLSearchParams === 'function' },
// Encoding API
{ name: 'TextEncoder', test: () => typeof TextEncoder === 'function' },
{ name: 'TextDecoder', test: () => typeof TextDecoder === 'function' },
{ name: 'btoa', test: () => typeof btoa === 'function' },
{ name: 'atob', test: () => typeof atob === 'function' },
// Crypto API
{ name: 'crypto', test: () => typeof crypto !== 'undefined' },
{ name: 'crypto.getRandomValues', test: () => typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function' },
{ name: 'crypto.randomUUID', test: () => typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' },
{ name: 'crypto.subtle', test: () => typeof crypto !== 'undefined' && typeof crypto.subtle !== 'undefined' },
// Performance API
{ name: 'performance', test: () => typeof performance !== 'undefined' },
{ name: 'performance.now', test: () => typeof performance !== 'undefined' && typeof performance.now === 'function' },
{ name: 'performance.mark', test: () => typeof performance !== 'undefined' && typeof performance.mark === 'function' },
{ name: 'performance.measure', test: () => typeof performance !== 'undefined' && typeof performance.measure === 'function' },
// Fetch API
{ name: 'fetch', test: () => typeof fetch === 'function' },
{ name: 'Request', test: () => typeof Request === 'function' },
{ name: 'Response', test: () => typeof Response === 'function' },
{ name: 'Headers', test: () => typeof Headers === 'function' },
// Streams API
{ name: 'ReadableStream', test: () => typeof ReadableStream === 'function' },
{ name: 'WritableStream', test: () => typeof WritableStream === 'function' },
{ name: 'TransformStream', test: () => typeof TransformStream === 'function' },
// File API
{ name: 'File', test: () => typeof File === 'function' },
{ name: 'Blob', test: () => typeof Blob === 'function' },
{ name: 'FileReader', test: () => typeof FileReader === 'function' },
// Storage API
{ name: 'localStorage', test: () => typeof localStorage !== 'undefined' },
{ name: 'sessionStorage', test: () => typeof sessionStorage !== 'undefined' },
// IndexedDB
{ name: 'indexedDB', test: () => typeof indexedDB !== 'undefined' },
{ name: 'IDBDatabase', test: () => typeof IDBDatabase === 'function' },
// WebSocket
{ name: 'WebSocket', test: () => typeof WebSocket === 'function' },
// Event API
{ name: 'Event', test: () => typeof Event === 'function' },
{ name: 'CustomEvent', test: () => typeof CustomEvent === 'function' },
{ name: 'EventTarget', test: () => typeof EventTarget === 'function' },
{ name: 'AbortController', test: () => typeof AbortController === 'function' },
{ name: 'AbortSignal', test: () => typeof AbortSignal === 'function' },
// DOM-like APIs (usually not in d8)
{ name: 'document', test: () => typeof document !== 'undefined' },
{ name: 'window', test: () => typeof window !== 'undefined' },
{ name: 'global', test: () => typeof global !== 'undefined' },
{ name: 'globalThis', test: () => typeof globalThis !== 'undefined' },
// Worker APIs
{ name: 'Worker', test: () => typeof Worker === 'function' },
{ name: 'SharedWorker', test: () => typeof SharedWorker === 'function' },
{ name: 'MessageChannel', test: () => typeof MessageChannel === 'function' },
{ name: 'MessagePort', test: () => typeof MessagePort === 'function' },
// Intl API (usually available)
{ name: 'Intl', test: () => typeof Intl !== 'undefined' },
{ name: 'Intl.DateTimeFormat', test: () => typeof Intl !== 'undefined' && typeof Intl.DateTimeFormat === 'function' },
{ name: 'Intl.NumberFormat', test: () => typeof Intl !== 'undefined' && typeof Intl.NumberFormat === 'function' },
{ name: 'Intl.Collator', test: () => typeof Intl !== 'undefined' && typeof Intl.Collator === 'function' },
// Other utility APIs
{ name: 'structuredClone', test: () => typeof structuredClone === 'function' },
{ name: 'queueMicrotask', test: () => typeof queueMicrotask === 'function' },
];
let supported = 0;
let total = webAPIs.length;
webAPIs.forEach(api => {
try {
const isSupported = api.test();
print(` ${api.name}: ${isSupported ? '✓ SUPPORTED' : '✗ NOT SUPPORTED'}`);
if (isSupported) supported++;
} catch (error) {
print(` ${api.name}: ✗ ERROR - ${error.message}`);
}
});
print(`\nWeb API Support Summary: ${supported}/${total} (${Math.round(supported/total*100)}%)`);
}
testWebAPICompatibility();
When I actually tried it, I got the following results:
Web API Compatibility in d8:
console: ✓ SUPPORTED
console.error: ✓ SUPPORTED
console.warn: ✓ SUPPORTED
console.info: ✓ SUPPORTED
console.debug: ✓ SUPPORTED
console.trace: ✓ SUPPORTED
console.assert: ✓ SUPPORTED
setTimeout: ✓ SUPPORTED
setInterval: ✗ NOT SUPPORTED
clearTimeout: ✗ NOT SUPPORTED
clearInterval: ✗ NOT SUPPORTED
setImmediate: ✗ NOT SUPPORTED
URL: ✗ NOT SUPPORTED
URLSearchParams: ✗ NOT SUPPORTED
TextEncoder: ✗ NOT SUPPORTED
TextDecoder: ✗ NOT SUPPORTED
btoa: ✗ NOT SUPPORTED
atob: ✗ NOT SUPPORTED
crypto: ✗ NOT SUPPORTED
crypto.getRandomValues: ✗ NOT SUPPORTED
crypto.randomUUID: ✗ NOT SUPPORTED
crypto.subtle: ✗ NOT SUPPORTED
performance: ✓ SUPPORTED
performance.now: ✓ SUPPORTED
performance.mark: ✓ SUPPORTED
performance.measure: ✓ SUPPORTED
fetch: ✗ NOT SUPPORTED
Request: ✗ NOT SUPPORTED
Response: ✗ NOT SUPPORTED
Headers: ✗ NOT SUPPORTED
ReadableStream: ✗ NOT SUPPORTED
WritableStream: ✗ NOT SUPPORTED
TransformStream: ✗ NOT SUPPORTED
File: ✗ NOT SUPPORTED
Blob: ✗ NOT SUPPORTED
FileReader: ✗ NOT SUPPORTED
localStorage: ✗ NOT SUPPORTED
sessionStorage: ✗ NOT SUPPORTED
indexedDB: ✗ NOT SUPPORTED
IDBDatabase: ✗ NOT SUPPORTED
WebSocket: ✗ NOT SUPPORTED
Event: ✗ NOT SUPPORTED
CustomEvent: ✗ NOT SUPPORTED
EventTarget: ✗ NOT SUPPORTED
AbortController: ✗ NOT SUPPORTED
AbortSignal: ✗ NOT SUPPORTED
document: ✗ NOT SUPPORTED
window: ✗ NOT SUPPORTED
global: ✗ NOT SUPPORTED
globalThis: ✓ SUPPORTED
Worker: ✓ SUPPORTED
SharedWorker: ✗ NOT SUPPORTED
MessageChannel: ✗ NOT SUPPORTED
MessagePort: ✗ NOT SUPPORTED
Intl: ✓ SUPPORTED
Intl.DateTimeFormat: ✓ SUPPORTED
Intl.NumberFormat: ✓ SUPPORTED
Intl.Collator: ✓ SUPPORTED
structuredClone: ✗ NOT SUPPORTED
queueMicrotask: ✗ NOT SUPPORTED
Web API Support Summary: 18/60 (30%)
It seems the supported features are as follows:
- Console API (all)
- setTimeout (partial - setInterval and clear-related functions are not supported)
- Performance API (all)
- globalThis
- Worker (basic only, SharedWorker is not supported)
- Intl API (major features)
Let's briefly try features other than the Console API.
$ out.gn/arm64.release/d8
V8 version 13.9.0 (candidate)
d8> setTimeout(() => console.log("hello"), 5000)
hello
0
d8> const start = performance.now();
undefined
d8> const end = performance.now();
undefined
d8> console.log(`${(end - start).toFixed(3)}ms precision`)
4354.649ms precision
undefined
d8> Object.keys(globalThis)
["version", "print", "printErr", "write", "writeFile", "read", "readbuffer", "readline", "load", "setTimeout", "quit", "Realm", "performance", "Worker", "os", "d8", "arguments"]
d8> const formatter = new Intl.DateTimeFormat("en-US")
undefined
d8> formatter.format(new Date())
"6/25/2025"
d8> quit()
Regarding setTimeout, it was executed immediately without waiting for 5000ms.
Troubleshooting
When the following error occurs during fetch v8:
error: RPC failed; HTTP 500 curl 22 The requested URL returned error: 500
Because of the large size, it seems that if there is a bandwidth limit while fetching via HTTPS, it might return a 500 error.
Therefore, run it with the --no-history flag (equivalent to --depth=1).
$ fetch --no-history v8
Discussion