Programming with AI
ENRU
12.1

What a build is and why it breaks

five reasons behind "it works on my machine"

What it is

A build is the turning of your sources into something that runs for other people. While you develop, a developer tool runs your program: it reads your files as they are, substitutes settings from your machine, reloads what changed on the fly. That is convenient and nothing like what the user will get.

During a build something else happens: the code is checked, rewritten, glued into a few files, setting values are substituted in, the surplus is thrown away. The result is an artifact — one file or folder you can hand over: an .apk for Android, a dist/ folder for a website, a .whl or a container for the server side.

The gap between "it ran on my machine" and "the artifact works" is the most frequent and most galling breakage in a project, because it happens at the end, when everything already looked finished.

Diagram from the book — to be redrawn for the web
Three states of one project. This chapter's errors live on the arrows, not in the code.

What it looks like in code

Below are five reasons for breakage, and all five were checked on live projects: the real command output is shown.

1. The build passed and the code is broken

Here is a file with two blatant type errors — a string where a number belongs and a number where a string belongs:

import { CityCard } from './components/CityCard'

export default function App() {
  const temp: number = "hot"
  return <CityCard name={temp} />
}

Run the build directly:

$ npx vite build
vite v7.3.7 building client environment for production...
✓ 28 modules transformed.
dist/index.html                  0.20 kB │ gzip:  0.17 kB
dist/assets/index-D40P3igU.js  222.62 kB │ gzip: 69.34 kB
✓ built in 1.69s

It built. In under two seconds, with a tick. Now the type check, on the same file:

$ npx tsc -b
src/App.tsx(4,9): error TS2322: Type 'string' is not assignable to type 'number'.
src/App.tsx(5,20): error TS2322: Type 'number' is not assignable to type 'string'.

The reason is that the bundler doesn't check types — it throws them away. Types are for the editor and the checker; they aren't in the built file (article 9.1). Which is why in a normal project the build command is two commands:

"build": "tsc -b && vite build"

Check first, then glue. And this is exactly where the mistake appears that both people and AIs make: the build fails, and it is "fixed" by taking tsc -b out of the command. The build turns green, the check is switched off.

2. Settings are baked in

Environment variables aren't read while the program runs — they are substituted into the text of the artifact. The settings file:

measured
VITE_API_URL=https://api.example.com
API_KEY=secret-abc-123

The code that reads them:

const API = import.meta.env.VITE_API_URL
const KEY = import.meta.env.API_KEY

And here is what ended up in the built file — a real fragment from dist/:

const mh="https://api.example.com",dh=void 0;

One line, two different disasters.

The first: the address got into the artifact as text. The built file is served to anyone's browser, which means everything in it is public. Had there been a key for a paid API in place of the address, it would be sitting right there in plain text.

The second: API_KEY became void 0, that is, "nothing". The bundler substitutes only variables with the right prefix (VITE_ in this example); the rest silently become empty. No error, no warning: the application builds, starts, and behaves oddly — knocking on undefined/api/cities, for instance.

You can check this by hand, and it is worth doing at least once in your life: build the project and search the built file for your own secret.

$ grep -c "secret-abc-123" dist/assets/*.js
0
$ grep -c "api.example.com" dist/assets/*.js
1

The rule is simple: what is meant to stay secret doesn't go into code that travels to the user's device. Keys live on the server, and the application talks to its own server, which talks to the paid API.

3. A path that depends on where you started from

An innocent line:

with open("weather/cities.json", encoding="utf-8") as f:

From the project root it works:

$ python3 -m weather.main
{'Oslo': 4, 'Tromso': -3}

From any other folder it doesn't:

measured
  File ".../weather/main.py", line 5, in load_cities
    with open("weather/cities.json", encoding="utf-8") as f:
         ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'weather/cities.json'

A relative path is counted not from the file with the code but from the folder the program was started in. During development that is always the project root, so the mistake is invisible. On a server the program is started by a service, and its folder is its own.

The right way is to count from the file's own location:

from pathlib import Path

DATA = Path(__file__).parent / "cities.json"

def load_cities():
    with DATA.open(encoding="utf-8") as f:
        return json.load(f)

After that edit, running from /tmp gives the same thing as running from the project root. Checked: {'Oslo': 4, 'Tromso': -3}.

4. A file that didn't make it into the artifact

The same project, paths now fixed, built into a package:

$ python3 -m build --wheel
Successfully built weather-0.1.0-py3-none-any.whl

No error, no warning. Look at what's inside:

measured
weather/__init__.py
weather/main.py
weather-0.1.0.dist-info/METADATA
weather-0.1.0.dist-info/WHEEL
weather-0.1.0.dist-info/RECORD

cities.json isn't in there. Install the package and run it:

measured
FileNotFoundError: [Errno 2] No such file or directory:
'/tmp/venv-weather/lib/python3.13/site-packages/weather/cities.json'

A build tool builds code, and everything else — images, fonts, dictionaries, email templates, data files — has to be declared separately. Three lines in the project's settings:

[tool.setuptools.package-data]
weather = ["*.json"]

And the built package now has what it needs:

measured
adding 'weather/__init__.py'
adding 'weather/cities.json'
adding 'weather/main.py'

This is a disease common to every stack: in Flutter assets are declared in pubspec.yaml (article 11.1), on the frontend images must be either imported or put in a special folder, into a container files have to be copied with an explicit command. Forget to declare it and the artifact is built without the file, and it fails not at build time but when the screen that needs that file is opened.

5. Dependency versions drift

The project's settings said this:

"react": "^19.1.1",
"vite": "^7.1.7"

And this is what got installed:

$ npm ls react vite --depth=0
+-- react@19.3.1
`-- vite@7.3.7

The ^ sign means "this version or newer within compatibility". That is, the same project installed by you today and on a server a month from now will get different versions of the libraries. Usually nothing happens. Sometimes something does, and working out what changed starts from scratch.

With Python it is the same, and vividly so: one requests line in the dependency list drags in five packages, each with its own version:

measured
certifi==2026.7.22
charset-normalizer==3.5.2
idna==3.20
requests==2.34.2
urllib3==2.8.0

Four of the five you didn't choose. They are the source of the surprises.

The cure is a file with exact versions, created automatically on install: package-lock.json, pubspec.lock, poetry.lock, a requirements.txt produced by pip freeze. It records exactly what you had when everything worked:

node_modules/react -> 19.3.1
node_modules/vite  -> 7.3.7

That file must be committed to the repository, and dependencies installed with a command that respects it: npm ci rather than npm install. Otherwise it is pointless.

What to tell the AI

Prompt
The project builds for me but fails after installation.
Don't propose fixes at random. First list how running in
development differs from the built artifact in this project:
file paths, environment variables, assets, dependency
versions. One point per line.

A request like that gives you a list of places to look instead of a random edit.

Prompt
Here is my build command and my environment settings file.
List which values will end up in the artifact in plain text
and be visible to the user. Separately: what will become
undefined, and why.
Prompt
List every file the project reads that isn't code: data,
images, fonts, templates. For each one say whether it's
declared in the build settings. If it isn't, write the line
that needs adding.
Prompt
The build fails with this error: <text>.
I want two options: a code fix and a build-settings fix.
For each, say what will stop being checked. I'll choose.

That last technique is worth remembering. Any "build fix" should come with its price: without that question, switching a check off looks like a solution.

What to look for

The check vanished from the build command. It was tsc -b && vite build, it is now vite build. The build is green, the types aren't checked.

Suppression flags appeared. --force, --legacy-peer-deps, --no-verify, --ignore-scripts, || true at the end of a command. Each of them switches something off, and that something has to be said out loud.

A secret in the application's code. An API key, a password, a token in a file that is built and travels to the device. Checked by searching the built file.

Relative paths to data files. open("data/x.json") instead of a path from __file__.

Undeclared assets. An image, a font or a data file is in the project folder but isn't mentioned in the build settings.

The exact-versions file in .gitignore. The classic "optimization": package-lock.json looks like junk because it is large and keeps changing. Without it there is no reproducible build.

Building on your machine as the only way. As long as the project builds only for you and only by hand, nobody — you in six months included — will be able to build it.

Cheat sheet

  • A build isn't a run: different code, different settings, different files.

  • A green build doesn't mean working code: the bundler throws types away rather than checking them.

  • Editing the build command instead of the code is nearly always switching a check off.

  • Environment settings are baked into the artifact: secrets don't belong there.

  • A variable without the right prefix silently becomes undefined.

  • A relative path is counted from the launch folder, not from the code file.

  • Everything that isn't code has to be declared in the build settings.

  • Check the installed package from a different folder, or you'll find the sources.

  • Commit the exact-versions file and install with npm ci, not install.