iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🛠️

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.

https://v8.dev/docs/d8

V8

https://v8.dev/

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

https://v8.dev/docs/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

https://v8.dev/docs/build-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 reading BUILD.gn files written in a custom DSL to quickly generate build.ninja files for the actual builder, Ninja.

  1. 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.

  1. 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