You are viewing a preview of this lesson. Sign in to start learning
Back to Hermetic Builds

The 'Works on My Machine' Problem

Explore how non-hermetic builds cause inconsistencies across developer machines and CI environments.

The 'Works on My Machine' Problem

Master hermetic builds with free flashcards and spaced repetition practice. This lesson covers the notorious "works on my machine" syndrome, environmental inconsistencies, and reproducibility challengesβ€”essential concepts for understanding why hermetic builds matter in modern software development.

Welcome πŸ‘‹

Every developer has experienced it: you write code that runs perfectly on your computer, but when a teammate tries to run itβ€”disaster. Tests fail, features break, or worse, the application won't even start. When you ask what went wrong, they shrug and say, "I don't know, it just doesn't work." You respond with the most frustrating phrase in software development: "But it works on my machine!" πŸ’»βŒ

This seemingly simple problem has cost the software industry billions of dollars in lost productivity, delayed releases, and emergency hotfixes. The "works on my machine" problem isn't just an inconvenienceβ€”it's a fundamental challenge that reveals the hidden complexities of software environments and the desperate need for hermetic builds.

Core Concepts: Understanding Environmental Chaos πŸŒͺ️

What is the "Works on My Machine" Problem?

The "works on my machine" problem (also called the "WOMM problem") occurs when software behaves differently across different environments, even when the source code is identical. This happens because software doesn't run in a vacuumβ€”it depends on a complex ecosystem of factors that vary from machine to machine.

Key definition: An environment in software development includes everything needed to build and run software: operating system, installed libraries, system configuration, environment variables, file system state, network conditions, and countless other variables.

The Anatomy of Environmental Differences πŸ”¬

Let's break down the common sources of environmental inconsistency:

Environmental Factor What Varies Common Symptom
Dependencies Library versions, package installations "Module not found" or "incompatible version"
System Configuration Environment variables, PATH settings "Command not found" or wrong tool version used
Operating System Windows vs macOS vs Linux differences File path separators, line endings, case sensitivity
File System State Cached files, temporary data, absolute paths "Build uses stale data" or "hardcoded path fails"
Network Access Available registries, download speeds, firewalls "Timeout fetching dependency" or wrong package version
Time & Locale System time, timezone, language settings "Date parsing fails" or encoding issues

Why Does This Matter? The Reproducibility Crisis πŸ“‰

Reproducibility means that given the same inputs, a process produces the same outputs every time. In software builds, this means:

βœ… Reproducible build: Same source code β†’ Same binary output (always) ❌ Non-reproducible build: Same source code β†’ Different binary outputs (chaos!)

When builds aren't reproducible, you face:

  • Debugging nightmares: "I can't reproduce the bug you're seeing"
  • Deployment disasters: "It worked in staging but failed in production"
  • Security vulnerabilities: "We can't verify this binary came from our source code"
  • Team friction: "Just works for me, not sure what you're doing wrong"
  • Wasted time: Hours spent on "environmental archaeology" instead of building features

πŸ’‘ Did you know? The Debian project found that only 83% of packages could be built reproducibly in 2015. By implementing hermetic build practices, they improved this to over 95% by 2021.

The Hidden Dependencies Problem πŸ•΅οΈ

Many builds have hidden dependenciesβ€”things your code relies on that aren't explicitly declared:

VISIBLE VS HIDDEN DEPENDENCIES

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  πŸ“¦ Your Project                    β”‚
β”‚                                     β”‚
β”‚  βœ… package.json (visible)          β”‚
β”‚     β”œβ”€ react: 18.2.0                β”‚
β”‚     └─ lodash: 4.17.21              β”‚
β”‚                                     β”‚
β”‚  ❌ Hidden dependencies:            β”‚
β”‚     β”œβ”€ Node.js version (16? 18?)   β”‚
β”‚     β”œβ”€ npm vs yarn vs pnpm          β”‚
β”‚     β”œβ”€ Global npm packages          β”‚
β”‚     β”œβ”€ System Python (some deps)   β”‚
β”‚     β”œβ”€ $PATH and $NODE_ENV vars    β”‚
β”‚     β”œβ”€ OS architecture (x64? ARM?) β”‚
β”‚     └─ Cached node_modules state   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

These hidden dependencies are the primary cause of the "works on my machine" problem. Your machine has them configured correctly (often by accident!), but your teammate's machine doesn't.

Real-World Examples: When Good Code Goes Bad 🎭

Example 1: The Python Path Mystery 🐍

Scenario: Sarah writes a Python script that imports a custom utility module.

Sarah's machine (works):

## main.py
import utils

result = utils.process_data([1, 2, 3])
print(result)

What Sarah doesn't realize:

  • She has export PYTHONPATH=/home/sarah/projects/lib in her .bashrc
  • The utils.py file is in that directory
  • Python finds it automatically on her machine

Jake's machine (fails):

$ python main.py
Traceback (most recent call last):
  File "main.py", line 1, in <module>
    import utils
ModuleNotFoundError: No module named 'utils'

Jake doesn't have that PYTHONPATH set, so Python can't find the module.

The hermetic solution: Make the dependency explicit:

## Instead of relying on PYTHONPATH:
## 1. Put utils.py in the project directory
## 2. Use relative imports
## 3. Create a proper package with setup.py
## 4. Document ALL requirements including Python version

Example 2: The Node.js Version Roulette 🎰

Scenario: A team builds a web application that works perfectly in development.

Developer's machine:

  • Node.js v18.12.0 (latest LTS)
  • Modern JavaScript features work perfectly

CI/CD server:

  • Node.js v14.17.0 (older version)
  • Build fails with syntax errors
// Code using optional chaining (Node 14+ feature)
const name = user?.profile?.name ?? 'Anonymous';

// Works on Node 18 βœ…
// Crashes on Node 12 ❌ (Unexpected token '?')

Why it happened:

  • No package.json engine specification
  • Developer accidentally used their global Node installation
  • CI server had old Node version pinned

The hermetic solution:

{
  "name": "my-app",
  "engines": {
    "node": ">=18.0.0",
    "npm": ">=8.0.0"
  },
  "scripts": {
    "preinstall": "node -e \"if(process.version<'v18')throw new Error('Need Node 18+')\""
  }
}

Use tools like nvm (Node Version Manager) or Docker to ensure everyone uses the same Node version.

Example 3: The Operating System Shuffle πŸ’Ώ

Scenario: A build script works on Linux but fails on Windows.

Linux (developer's machine):

#!/bin/bash
## build.sh
find src/ -name "*.ts" | xargs tsc
cat dist/*.js > bundle.js

Windows (colleague's machine):

'find' is not recognized as an internal or external command
'cat' is not recognized as an internal or external command

The problems:

  • Shell script syntax (Bash vs PowerShell vs Command Prompt)
  • Different path separators (/ vs \)
  • Different line endings (LF vs CRLF)
  • Different available commands

The hermetic solution: Use cross-platform tools:

// build.js (Node.js - runs everywhere)
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

// Cross-platform path handling
const srcDir = path.join(__dirname, 'src');

// Use Node.js APIs instead of shell commands
const files = fs.readdirSync(srcDir)
  .filter(f => f.endsWith('.ts'))
  .map(f => path.join(srcDir, f));

// Use a cross-platform build tool
execSync(`tsc ${files.join(' ')}`);

Example 4: The Dependency Time Bomb ⏰

Scenario: Code that worked yesterday suddenly breaks today, with no changes to your codebase.

Yesterday:

// package.json
{
  "dependencies": {
    "some-library": "^2.1.0"
  }
}

What happened overnight:

  • Library maintainer published version 2.2.0
  • It has a breaking API change (accidentally or intentionally)
  • Your ^2.1.0 means "any version 2.x.x is okay"
  • New developer runs npm install and gets 2.2.0
  • Everything breaks

The hermetic solution:

  1. Use exact versions (remove the ^):
{
  "dependencies": {
    "some-library": "2.1.0"
  }
}
  1. Commit your lock file (package-lock.json, yarn.lock, etc.):
## .gitignore - WRONG!
package-lock.json  ❌ Don't ignore this!

## Instead, commit it:
git add package-lock.json  βœ…
  1. Use a private registry or vendoring to control when updates happen

Why Traditional Solutions Fall Short ⚠️

Many teams try to solve the "works on my machine" problem with documentation:

The "README solution":

## Setup Instructions

1. Install Node.js 18
2. Install Python 3.9
3. Set JAVA_HOME to your JDK installation
4. Install these global npm packages: ...
5. Configure your PATH to include: ...
6. Run these 15 commands in order: ...

Why this fails:

  • πŸ“„ Documentation drift: README gets outdated as project evolves
  • πŸ‘₯ Human error: Developer skips a step or does it wrong
  • πŸ”„ Not repeatable: Manual steps = inconsistent results
  • ⏰ Time-consuming: Hours to set up on each new machine
  • πŸ› Debugging hell: When it fails, which step went wrong?

The Path to Hermeticity 🎯

The "works on my machine" problem is fundamentally a problem of environmental couplingβ€”your build is too tightly coupled to the specific state of one developer's machine.

Hermetic builds solve this by:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  TRADITIONAL BUILD (coupled)             β”‚
β”‚                                          β”‚
β”‚  Source Code ──┬──→ Dev Machine A ──→ βœ… β”‚
β”‚                β”‚                         β”‚
β”‚                β”œβ”€β”€β†’ Dev Machine B ──→ ❌ β”‚
β”‚                β”‚                         β”‚
β”‚                └──→ CI Server ─────→ ❓ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  HERMETIC BUILD (isolated)               β”‚
β”‚                                          β”‚
β”‚  Source Code ──┬──→ Isolated Env ──→ βœ…  β”‚
β”‚    +           β”‚      (same)             β”‚
β”‚  All Deps      β”œβ”€β”€β†’ Isolated Env ──→ βœ…  β”‚
β”‚  Explicitly    β”‚      (same)             β”‚
β”‚  Declared      └──→ Isolated Env ──→ βœ…  β”‚
β”‚                                          β”‚
β”‚  Same inputs β†’ Same environment          β”‚
β”‚              β†’ Same output (always!)     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key principles of hermetic builds:

  1. πŸ”’ Isolation: Build doesn't access anything outside its declared inputs
  2. πŸ“‹ Explicit dependencies: All requirements are declared and versioned
  3. πŸ”„ Reproducibility: Same inputs β†’ same outputs (always)
  4. ⏱️ Determinism: No randomness, timestamps, or network-dependent behavior
  5. 🚫 No side effects: Build doesn't modify system state

πŸ’‘ Think of it like this: A hermetic build is like a pure function in programmingβ€”given the same inputs, it always produces the same output, with no hidden dependencies or side effects.

Common Mistakes to Avoid ⚠️

Mistake 1: Relying on Global Installations ❌

Wrong:

## Assuming user has globally installed tools
npm install -g typescript
tsc src/index.ts

Right:

## Project-local dependencies
npm install --save-dev typescript
npx tsc src/index.ts  # Uses local version

Mistake 2: Not Version-Locking Dependencies ❌

Wrong:

{
  "dependencies": {
    "react": "*",        // Any version! 😱
    "lodash": "^4.17.0"  // Could get 4.99.0 tomorrow
  }
}

Right:

{
  "dependencies": {
    "react": "18.2.0",
    "lodash": "4.17.21"
  }
}
// AND commit your lock file!

Mistake 3: Hardcoding Absolute Paths ❌

Wrong:

## Hardcoded path to YOUR machine
config = open('/Users/sarah/projects/myapp/config.json')

Right:

import os
from pathlib import Path

## Relative to script location
config_path = Path(__file__).parent / 'config.json'
config = open(config_path)

Mistake 4: Depending on Network During Build ❌

Wrong:

## Build fetches from internet (network failure = build failure)
curl https://example.com/asset.js > vendor.js

Right:

## Assets committed to repo or fetched once and cached
## Build uses local copies only
cp vendor/asset.js dist/

Mistake 5: Using Timestamps or Random Values ❌

Wrong:

// Different output every time!
const buildId = Date.now();
fs.writeFileSync('build.json', JSON.stringify({ buildId }));

Right:

// Deterministic identifier from git commit
const buildId = execSync('git rev-parse HEAD').toString().trim();
fs.writeFileSync('build.json', JSON.stringify({ buildId }));

Key Takeaways πŸŽ“

βœ… The "works on my machine" problem occurs when software behaves differently across environments due to environmental inconsistencies

βœ… Hidden dependencies (environment variables, global tools, system configuration) are the primary culprit

βœ… Reproducibility means same inputs β†’ same outputs, which is essential for reliable software development

βœ… Documentation alone is insufficientβ€”human error and drift make manual setup unreliable

βœ… Hermetic builds solve this by isolating the build environment and making all dependencies explicit

βœ… Key hermetic principles: Isolation, explicit dependencies, reproducibility, determinism, no side effects

Quick Reference Card πŸ“‹

πŸ“‹ WOMM Problem Checklist

βœ… Explicit dependencies All tools and libraries declared with versions
βœ… Lock files committed package-lock.json, Pipfile.lock, etc. in version control
βœ… Local tooling No reliance on global installations
βœ… Relative paths No hardcoded absolute paths to specific machines
βœ… Cross-platform code Works on Windows, macOS, Linux
βœ… Deterministic builds No timestamps, random values, or network dependencies
βœ… Documented requirements Clear system requirements (OS version, architecture)
βœ… Automated setup Scripts/containers for consistent environment creation

πŸ€” Did You Know?

The phrase "works on my machine" became so notorious in software development that it spawned countless memes, t-shirts, and even a satirical certification badge that says "Works on My Machine - Certified." Docker, one of the most popular containerization tools, directly addresses this problem with their tagline: "But it works on my machine... then we'll ship your machine!"

πŸ“š Further Study


Next in the roadmap: You'll learn about the specific properties that make a build truly hermetic and how to achieve them in your projects.