Hermetic Environment
What is Hermeticity?
The word "hermetic" comes from Hermes Trismegistus, a legendary figure in alchemy who was said to have the power to seal vessels so perfectly that nothing could escape. In modern usage, "hermetically sealed" means completely airtight and isolated from external influences - like a vacuum-sealed container in a laboratory.
In software builds, hermeticity means creating a build environment that's completely sealed off from the surrounding system, containing everything it needs within itself. This means that given the same inputs, you'll get the same outputs every time, regardless of where or when you run the build.
Why Hermeticity Matters
Consider these common build problems:
- "Works on my machine" but fails in CI
- Builds that depend on system-installed libraries
- Tests that pass locally but fail in production
- Security vulnerabilities from untrusted dependencies
- Flaky tests due to timestamp or environment differences
Hermeticity solves these by ensuring your build environment is:
- Complete - All dependencies are explicitly declared
- Isolated - No interference from the host system
- Reproducible - Same inputs always produce same outputs
- Secure - No untrusted or undeclared code execution
How Bazel Ensures Hermeticity
Out-of-Tree Builds
Unlike traditional build systems that create artifacts alongside source files ("in-tree"), Bazel strictly separates source and output:
workspace/
├── src/
│ ├── BUILD
│ └── main.cc
└── bazel-bin/ # Separate build output directory
└── src/
└── app # Built binaryThis separation:
- Prevents accidental dependencies on build artifacts
- Makes it impossible to use files that aren't declared
- Ensures build reproducibility
- Makes it easy to clean builds (just delete bazel-* directories)
Sandboxing
Bazel sandboxes each build action to ensure hermeticity:
# Each action runs in its own sandbox with:
# 1. Only declared inputs available
# 2. Limited system access
# 3. Controlled environment variables
cc_binary(
name = "app",
srcs = ["app.cc"],
data = ["config.txt"], # Must declare ALL needed files
)On Linux, Bazel uses:
- Mount namespaces for filesystem isolation
- Process namespaces for process isolation
- Network namespaces for network control
- Resource limits for CPU/memory control
Dependency Detection
Bazel enforces explicit dependency declaration:
# This will fail - header dependency not declared
cc_library(
name = "lib",
srcs = ["lib.cc"],
# Missing hdrs = ["lib.h"]
)
# This works - all dependencies explicit
cc_library(
name = "lib",
srcs = ["lib.cc"],
hdrs = ["lib.h"],
deps = ["//third_party/json"],
)Environment Control
Bazel controls the build environment through explicit configuration:
# In .bazelrc - define a controlled environment
build --incompatible_strict_action_env
build --action_env=PATH=/bin:/usr/bin
build --action_env=LANG=C.UTF-8Why Not Just Use Containers for Builds?
While containers like Docker provide isolation, they're not optimized for build systems. Here's why Bazel takes a different approach:
1. Build Performance
Container Issues:
- Layer-based caching is too coarse
- Must rebuild entire layer if any file changes
- Uses VMs on macOS/Windows which makes builds much slower (cannot use the full machine)
- Network translation adds latency
Bazel's Approach:
# Bazel caches at the action level
cc_binary(
name = "app",
srcs = ["app.cc"],
deps = ["//lib"], # Only rebuilds when dependencies change
)2. Dependency Management
Container Issues:
# Container makes ALL tools available to ALL builds
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
build-essential \
python3 \
nodejs
# Can't track what each build actually needsBazel's Approach:
# Each target declares exactly what it needs
cc_binary(
name = "app",
srcs = ["app.cc"],
deps = ["//third_party/protobuf"],
)
py_binary(
name = "script",
srcs = ["script.py"],
deps = ["//third_party/requests"],
)3. Cross-Platform Builds
Container Issues:
- Different behavior on Linux vs macOS/Windows
- Network structure varies by platform
- Filesystem access has different semantics
- Resource limits work differently
Bazel's Approach:
# Same behavior everywhere
platform(
name = "linux_x86_64",
constraint_values = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
],
)
# Cross-compilation just works
build --platforms=//platforms:arm644. Build Correctness
Container Issues:
- Can accidentally use undeclared files
- Network access is hard to control
- Environment variables leak through
- No cryptographic verification
Bazel's Approach:
# Every input must be declared
cc_binary(
name = "app",
srcs = ["app.cc"],
data = ["config.txt"],
)
# Dependencies are verified
http_archive(
name = "rules_cc",
urls = ["https://github.com/.../rules_cc-1.0.0.tar.gz"],
sha256 = "abc123...",
)Grades of Hermeticity
Not all builds are equally hermetic. Here's the spectrum from most to least hermetic:
Grade 1: Fully Hermetic
- All dependencies statically linked
- No system calls except basic I/O
- No environment variables
- No network access
- Reproducible bit-for-bit
Example: Security-critical binaries, blockchain validators
Grade 2: System Libc Only
- Dynamic libc, static everything else
- Limited system calls
- Controlled environment variables
- No network access
Example: Command-line tools, system utilities
Grade 3: System Dependencies
- Uses system libraries
- Controlled environment
- Limited network access (only for dependencies)
- Verified external dependencies
Example: Most application builds
Grade 4: Environment Dependent
- Uses system tools
- Depends on environment variables
- Network access during build
- Some non-reproducible outputs
Example: Development builds, prototypes
