Программирование с ИИ
ENRU
12.1

Что такое сборка и почему она ломается

пять причин, по которым «у меня работает»

Что это

Сборка — это превращение твоих исходников в то, что запускается у других. Пока ты разрабатываешь, программу запускает инструмент разработчика: он читает твои файлы как есть, подставляет настройки с твоей машины, перезагружает изменённое на ходу. Это удобно и совсем не похоже на то, что получит пользователь.

При сборке происходит другое: код проверяется, переписывается, склеивается в несколько файлов, значения настроек подставляются внутрь, лишнее выбрасывается. Получается артефакт — один файл или папка, которые можно отдать: .apk для Android, папка dist/ для сайта, .whl или контейнер для серверной части.

Разница между «запустилось у меня» и «работает артефакт» — самая частая и самая обидная поломка в проекте, потому что она случается в конце, когда всё уже казалось готовым.

Схема из книги — будет перерисована для сайта
Три состояния одного проекта. Ошибки этой главы живут на стрелках, а не в коде.

Как выглядит в коде

Ниже пять причин поломок, и все пять проверены на живых проектах: показан настоящий вывод команд.

1. Сборка прошла, а код сломан

Вот файл с двумя явными ошибками типов — строка на месте числа и число на месте строки:

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

export default function App() {
  const temp: number = "жарко"
  return <CityCard name={temp} />
}

Запускаем сборку напрямую:

$ npx vite build
vite v7.3.7 building client environment for production...
✓ 29 modules transformed.
dist/index.html                  0.39 kB │ gzip:  0.27 kB
dist/assets/index-DSlY0u7I.js  222.62 kB │ gzip: 69.35 kB
✓ built in 1.96s

Собралось. За две секунды, с галочкой. Теперь проверка типов, на том же файле:

$ 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'.

Причина в том, что сборщик не проверяет типы — он их выбрасывает. Типы нужны редактору и проверяльщику, а в собранном файле их нет (статья 9.1). Поэтому в нормальном проекте команда сборки состоит из двух:

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

Сначала проверка, потом склейка. И вот ровно здесь появляется ошибка, которую допускают и люди, и ИИ: сборка падает, её «чинят», убрав из команды tsc -b. Сборка становится зелёной, проверка — выключенной.

2. Настройки запекаются внутрь

Переменные окружения при сборке не читаются в момент работы программы — они подставляются в текст артефакта. Файл настроек:

измерено
VITE_API_URL=https://api.example.com
API_KEY=secret-abc-123

Код, который их читает:

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

А вот что получилось в собранном файле — настоящий фрагмент из dist/:

var yh=oh();const mh="https://api.example.com",dh=void 0;

В одной строке сразу две разные беды.

Первая: адрес попал в артефакт текстом. Собранный файл отдаётся браузеру любого желающего, то есть всё, что в нём есть, — публично. Если бы на месте адреса был ключ от платного API, он лежал бы там же, открытым текстом.

Вторая: API_KEY превратился в void 0, то есть в «ничего». Сборщик подставляет только переменные с нужным префиксом (VITE_ в этом примере), остальные молча становятся пустыми. Ни ошибки, ни предупреждения: приложение собирается, запускается и ведёт себя странно — например, стучится по адресу undefined/api/cities.

Проверить это можно руками, и это стоит сделать хотя бы раз в жизни: собрать проект и поискать в собранном файле свой секрет.

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

Правило простое: то, что должно остаться тайной, не попадает в код, который уходит на устройство пользователя. Ключи живут на сервере, а приложение обращается к своему серверу, а тот — к платному API.

3. Путь, который зависит от того, откуда запустили

Невинная строка:

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

Из корня проекта работает:

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

Из любой другой папки — нет:

измерено
  File ".../pogoda/main.py", line 5, in load_cities
    with open("pogoda/cities.json", encoding="utf-8") as f:
         ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'pogoda/cities.json'

Относительный путь считается не от файла с кодом, а от папки, из которой запустили программу. При разработке это всегда корень проекта, поэтому ошибки не видно. На сервере программу запускает служба, и папка у неё своя.

Правильно — считать от расположения самого файла:

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)

После такой правки запуск из /tmp даёт то же, что из корня проекта. Это проверено: {'Oslo': 4, 'Tromso': -3}.

4. Файл не попал в артефакт

Тот же проект, уже с исправленными путями, собираем в пакет:

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

Ни ошибки, ни предупреждения. Смотрим, что внутри:

измерено
pogoda/__init__.py
pogoda/main.py
pogoda-0.1.0.dist-info/METADATA
pogoda-0.1.0.dist-info/WHEEL
pogoda-0.1.0.dist-info/RECORD

cities.json внутри нет. Устанавливаем пакет и запускаем:

измерено
FileNotFoundError: [Errno 2] No such file or directory:
'/tmp/venv-pogoda/lib/python3.13/site-packages/pogoda/cities.json'

Сборщик собирает код, а всё остальное — картинки, шрифты, словари, шаблоны писем, файлы данных — надо объявлять отдельно. Три строки в настройках проекта:

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

И в собранном пакете появляется нужное:

измерено
adding 'pogoda/__init__.py'
adding 'pogoda/cities.json'
adding 'pogoda/main.py'

Это общая болезнь всех стеков: во Flutter ресурсы объявляются в pubspec.yaml (статья 11.1), на фронтенде картинки надо либо импортировать, либо класть в особую папку, в контейнер файлы надо копировать явной командой. Забыли объявить — артефакт собрался без файла, и упадёт не при сборке, а при открытии того экрана, где этот файл нужен.

5. Версии зависимостей плавают

В настройках проекта было написано так:

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

А установилось вот это:

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

Значок ^ означает «эта версия или новее в пределах совместимости». То есть один и тот же проект, установленный у тебя сегодня и на сервере через месяц, получит разные версии библиотек. Обычно ничего не случается. Иногда случается, и выяснять, что именно изменилось, приходится с нуля.

С Python то же самое, и наглядно: одна строка requests в списке зависимостей притаскивает пять пакетов, каждый со своей версией:

измерено
certifi==2026.7.22
charset-normalizer==3.5.2
idna==3.20
requests==2.34.2
urllib3==2.8.0

Четыре из пяти ты не выбирал. Они и есть источник сюрпризов.

Лекарство — файл с точными версиями, который создаётся при установке автоматически: package-lock.json, pubspec.lock, poetry.lock, requirements.txt, собранный через pip freeze. В нём записано ровно то, что стояло у тебя, когда всё работало:

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

Этот файл надо коммитить в репозиторий и устанавливать зависимости командой, которая его уважает: npm ci вместо npm install. Иначе он бессмыслен.

Как сказать ИИ

Запрос
Проект собирается у меня, но падает после установки.
Не предлагай правок наугад. Сначала перечисли, чем
запуск при разработке отличается от собранного артефакта
в этом проекте: пути к файлам, переменные окружения,
ресурсы, версии зависимостей. По пункту на строку.

Такой запрос даёт список мест, где искать, вместо случайной правки.

Запрос
Вот моя команда сборки и файл настроек окружения.
Перечисли, какие значения попадут в артефакт открытым
текстом и будут видны пользователю. Отдельно — что
станет undefined и почему.
Запрос
Перечисли все файлы, которые проект читает не как код:
данные, картинки, шрифты, шаблоны. Для каждого скажи,
объявлен ли он в настройках сборки. Если не объявлен —
напиши строку, которую надо добавить.
Запрос
Сборка падает с этой ошибкой: <текст>.
Нужны два варианта: правка кода и правка настроек сборки.
Для каждого скажи, что именно перестанет проверяться.
Выбирать буду я.

Последний приём стоит запомнить. Любую «правку сборки» надо получать вместе с ценой: без этого вопроса отключение проверки выглядит как решение.

На что смотреть

Из команды сборки пропала проверка. Было tsc -b && vite build, стало vite build. Сборка зелёная, типы не проверяются.

Появились флаги подавления. --force, --legacy-peer-deps, --no-verify, --ignore-scripts, || true в конце команды. Каждый из них что-то выключает, и это «что-то» надо назвать вслух.

Секрет в коде приложения. Ключ API, пароль, токен в файле, который собирается и уходит на устройство. Проверяется поиском по собранному файлу.

Относительные пути к файлам данных. open("data/x.json") вместо пути от __file__.

Ресурсы не объявлены. Картинка, шрифт или файл данных есть в папке проекта, но не упомянут в настройках сборки.

Файл с точными версиями в .gitignore. Классическая «оптимизация»: package-lock.json выглядит как мусор, потому что большой и меняется. Без него воспроизводимой сборки нет.

Сборка на твоей машине как единственный способ. Пока проект собирается только у тебя и только руками, никто — включая тебя через полгода — не сможет его собрать.

Шпаргалка

  • Сборка — это не запуск: другой код, другие настройки, другие файлы.

  • Зелёная сборка не значит рабочий код: сборщик типы выбрасывает, а не проверяет.

  • Правка команды сборки вместо кода — почти всегда отключение проверки.

  • Настройки окружения запекаются в артефакт: секретам там не место.

  • Переменная без нужного префикса молча превращается в undefined.

  • Относительный путь считается от папки запуска, а не от файла с кодом.

  • Всё, что не код, надо объявлять в настройках сборки отдельно.

  • Проверяй установленный пакет из другой папки, иначе найдёшь исходники.

  • Файл с точными версиями коммить и ставить через npm ci, а не install.