В удалённом Mac CI стабильный инструментальный стек проходит сборку, а после перехода на Swift 6.4 впервые появляется duplicate module name.
На этой неделе не пересоздавайте узел и не делайте полный откат: сначала найдите два доступных module.modulemap, проверьте поисковые пути и только затем удаляйте дубликат, обновляйте зависимость или переименовывайте собственный модуль. Если сторонний SDK пока нельзя изменить, оставьте стабильную линию для production, а Swift 6.4 проверяйте параллельно на изолированном узле.
Эта инструкция предназначена для разработчиков, которые поддерживают Swift-проекты с Objective-C, C/C++ и бинарными SDK. Она также пригодится DevOps- и build-инженерам, отвечающим за удалённый Mac CI, кэши зависимостей и переключение версий Xcode. Руководителям платформ важно дочитать раздел о критериях: там указано, когда исправлять зависимость, когда временно удерживать две линии и когда откладывать миграцию.
Важно: конкретный текст ошибки, затронутая библиотека и успешность исправления должны подтверждаться логом вашего проекта. Ниже описан метод локализации конфликта, а не универсальное утверждение, что любой сбой после Swift 6.4 вызван одним и тем же компонентом.
SECTION 01 Сначала установите границу сбоя, а не очищайте весь узел
Временная шкала диагностики должна начинаться с первого полезного сообщения компилятора. Запишите:
- последний успешный коммит и используемый инструментальный стек;
- первый неуспешный коммит после обновления;
- полную команду сборки, включая
-I,-F,-Xcc,-Xswiftcи переменные окружения; - состояние
Package.resolved, версию каждого бинарного SDK и используемый SDK; - полный вывод начиная с первого диагностического сообщения, а не только код завершения процесса.
Нужно отличить четыре разных класса проблем:
| Наблюдение в логе | Что это означает | Первое действие |
|---|---|---|
duplicate module name |
В одной области сканирования доступны два Clang module с одинаковым именем | Найти имена и реальные пути module.modulemap |
redefinition |
Повторно объявлен символ, тип или макрос | Проверить заголовки, include-порядок и guards |
module not found |
Компилятор не видит ожидаемый модуль | Проверить поисковые пути и доступность SDK |
| Ошибка на этапе link | Модули уже прошли компиляцию, проблема связана с объектами или библиотеками | Проверить -L, -l, архитектуру и порядок линковки |
Swift module и Clang module нельзя смешивать. Swift-модуль обычно описывает Swift-интерфейс и его бинарные артефакты, а Clang module строится вокруг заголовков и module map. module.modulemap — это декларация для Clang, а Module Cache — скомпилированное состояние, которое может сохранить старый результат. Наконец, ошибка линковщика возникает позже и не доказывает, что причиной был конфликт модулей.
На дату 8 сентября 2026 года официальные системные требования Apple указывают Xcode 27 beta 6 со Swift 6.4, а заметки к Xcode 27 описывают требование уникальности имён доступных Clang module в рамках одного dependency scan. Это подтверждает направление проверки, но не превращает поведение beta в окончательное правило для будущей финальной версии. Сверяйте статус версии с заметками к выпуску Xcode 27 и официальными системными требованиями Xcode.
Этап до конца рабочего дня: сохраните два лога — успешный на стабильном стеке и неуспешный на Swift 6.4. Без этой пары легко принять изменение окружения за изменение исходного кода.
SECTION 02 Где обычно возникает двойное объявление Clang module
Первый источник — собственная оболочка проекта. Проверьте все module.modulemap в репозитории, generated headers, bridging header и параметры target:
find <REPOSITORY_ROOT> -name module.modulemap -print
grep -R "module <MODULE_NAME>" \
<REPOSITORY_ROOT> <DEPENDENCY_ROOT> 2>/dev/null
Замените <MODULE_NAME>, <REPOSITORY_ROOT> и <DEPENDENCY_ROOT> фактическими значениями; не переносите эти placeholders в production-скрипт без проверки. Поиск по имени файла недостаточен: два map-файла могут называться одинаково, но объявлять разные модули, а один map может содержать несколько деклараций.
Затем сопоставьте три признака:
- точное имя
module; - список заголовков или
umbrella header; - абсолютный путь, который попал в команду компилятора.
Если собственный модуль дублирует модуль зависимости, предпочтительный ремонт — объединить декларации в одном контролируемом месте или переименовать собственный модуль с обновлением импортов. Простое удаление случайного каталога опасно: следующая установка зависимостей вернёт его, а локальная машина и CI снова разойдутся.
Проверьте также Header Search Paths. Один и тот же каталог, добавленный напрямую и через переменную, может сделать второй источник видимым. В настройках target ищите не только значение, но и способ его передачи через .xcconfig, скрипт сборки или аргументы xcodebuild. Документация Apple по настройкам target полезна для сверки фактических параметров, но итоговым доказательством остаётся команда из CI-лога.
Для системных библиотек сверяйте структуру map и публичных заголовков с документацией Swift Package Manager по system library dependencies. Не меняйте системный модуль только потому, что его имя совпало с именем сторонней библиотеки: сначала докажите, какой компонент добавил декларацию.
SECTION 03 Сторонние SDK, XCFramework и поисковые пути удалённого Mac CI
Вторая группа причин появляется на границе зависимостей. Один SDK может поставляться с module map, второй — включать скопированные заголовки и собственный map, а ручная интеграция добавляет ещё один путь к тому же содержимому. Отдельно проверяйте:
- vendored source с папкой
Headersи соседнимModules; XCFramework, где map находится внутри конкретного варианта платформы;- SwiftPM-зависимость и её checkout-каталог;
- SDK, добавленный вручную и одновременно подключённый через менеджер пакетов;
- скрипты, которые копируют заголовки во временный каталог перед сборкой.
Для каждого совпадения создайте короткую цепочку происхождения: компонент → путь → имя модуля → список заголовков → параметр, сделавший путь видимым. Рекомендации по устройству Clang modules и правилам их загрузки сверяйте с официальным описанием модулей Swift Clang и документацией Clang Modules.
Здесь важно различить два случая. Если два сторонних компонента используют одно имя по ошибке, ищите совместимое обновление у владельца зависимости и временно изолируйте конфликтующий SDK. Если библиотека повторно объявляет имя системного модуля, не маскируйте проблему переименованием системных импортов: это уже вопрос совместимости упаковки SDK.
Бинарный SDK нельзя безопасно исправлять так же, как исходную зависимость. Временный патч допустим только в отдельной ветке или слое сборки с зафиксированным diff, журналом происхождения и понятным способом отката. Не кладите непроверенный map-файл в основной production-репозиторий: такой обход может скрыть конфликт на одном target и сломать другой.
SECTION 04 Почему локальная машина чиста, а удалённый Mac CI видит конфликт
Удалённый узел часто получает дополнительные пути из окружения. Сравните локальную и CI-среду по следующим направлениям:
- выбранный SDK и активный путь к Xcode;
PATH,SDKROOT,CPATH,CPLUS_INCLUDE_PATHи переменные скриптов;- каталоги Homebrew и пользовательские toolchain;
- рабочий каталог агента;
- содержимое
Package.resolved; - параметры
xcodebuildи generated.xcconfig; - учётную запись, shell и файл инициализации.
Не делайте вывод «на CI есть лишний файл» только по результату find. Важен путь, который действительно прочитал компилятор. Используйте диагностику загрузки модулей, сохраните команду и сопоставьте её с официальными материалами Swift по диагностике компилятора. Если в вашем проекте применяется дополнительный флаг Clang, проверьте его действие в изолированной команде до изменения общего шаблона pipeline.
Различия аккаунтов тоже существенны. Интерактивная SSH-сессия может загружать один shell-профиль, а агент CI — другой. Рабочий каталог способен изменить относительный путь к заголовкам, а пользовательский кэш — предоставить модуль, которого нет в чистом checkout. Поэтому в лог нужно писать не секреты, а безопасный снимок: активный toolchain, SDK, рабочий каталог, нормализованные поисковые пути и идентификатор lock-файла.
В руководстве по удалённому Mac CI стоит заранее учитывать, что постоянный узел удобен для воспроизводимой среды только при явной фиксации инструментов и каталогов. Сам факт удалённого доступа не исправляет неоднозначность зависимостей; он лишь делает различия между агентами заметнее.
SECTION 05 Кэш: проверка состояния без разрушения всего узла
DerivedData и Module Cache могут сохранить результат старого сканирования. Но кэш не создаёт две декларации из ничего: если ошибка повторяется в новом каталоге с независимым кэшем, источник нужно искать в зависимостях или параметрах.
Безопасная последовательность выглядит так:
- Создайте новый рабочий каталог для того же коммита.
- Укажите отдельные пути DerivedData и кэша модулей.
- Запустите сборку с тем же SDK, lock-файлом и аргументами.
- Сохраните лог загрузки модуля и фактические пути.
- Только после сравнения удаляйте ограниченный каталог старого кэша.
- Повторите холодную, затем инкрементальную сборку.
Пример с placeholders:
xcodebuild \
-workspace <WORKSPACE> \
-scheme <SCHEME> \
-derivedDataPath <ISOLATED_DERIVED_DATA> \
-clonedSourcePackagesDirPath <ISOLATED_PACKAGES> \
clean build
Проверьте синтаксис вашей версии xcodebuild и не подставляйте общий каталог для параллельных jobs. Массовая очистка всего узла может прервать другие сборки, удалить полезные диагностические артефакты и искусственно изменить результат следующего запуска. Если очистка обязательна, ограничьте её job-каталогом и заранее определите способ восстановления кэша.
SECTION 06 Решение по временной шкале: исправлять, удерживать или откладывать
После локализации примените условия, а не интуицию:
- Если оба map-файла принадлежат вашему проекту или управляемой зависимости, то объедините декларации либо переименуйте собственный модуль; затем запускайте холодную и инкрементальную сборку.
- Если конфликт исправлен обновлением зависимости, то зафиксируйте новую версию в lock-файле и повторите проверку на чистом checkout.
- Если источник находится в бинарном SDK и у вас нет проверенного патча, то оставьте стабильный стек для production, а Swift 6.4 перенесите на отдельную линию совместимости.
- Если ошибка исчезает только после очистки старого кэша, то не считайте проблему решённой: повторите сборку с новым кэшем и после перезапуска job.
- Если стабильный стек и Swift 6.4 видят разные пути, то сначала выровняйте окружение, а не меняйте исходный код.
- Если источник модуля уникален, чистая сборка проходит, инкрементальная сборка также проходит, а повторный запуск даёт тот же результат, то можно расширять тестовую выборку и планировать перевод.
- Если любой из этих критериев не выполнен, то сохраняйте двойную CI-линию и не заменяйте рабочий production-узел.
Для такой схемы удалённый Mac полезен как быстро сбрасываемая среда проверки, особенно когда требуется одновременно держать стабильный Xcode и экспериментальный Swift 6.4. Но стабильность определяется не удалённым доступом, а неизменным checkout, прозрачными параметрами и сохранёнными логами. В руководстве по изоляции нескольких версий Xcode ищите подход к разделению toolchain и рабочих каталогов, а не совет по безусловной очистке всего хоста.
Контрольные точки перед расширением CI
На первой контрольной точке у вас должны быть два сравнимых лога. На второй — таблица происхождения всех совпадающих module map. На третьей — результат холодного и инкрементального запуска после изменения. На четвёртой — решение владельца зависимости или документированный временный обход.
Если проект собирает смешанные Swift, Objective-C и C/C++ targets, проверяйте каждый target отдельно. Успешная сборка одного приложения не доказывает, что тестовый target или вспомогательный framework больше не видит вторую декларацию.
SECTION 07 Частые вопросы
Развёрнутые ответы на четыре наиболее частых сценария собраны в FAQ выше: там отдельно разобраны различие между старым инструментом и Swift 6.4, поиск двух module.modulemap, роль DerivedData и выбор между откатом и двойной CI-линией.
Короткое правило можно сформулировать так: ошибка duplicate module name — это повод доказать происхождение модулей, а не повод немедленно удалять кэш. Сначала фиксируйте входные данные сборки, затем проверяйте пути, после этого меняйте зависимость и только в конце принимайте решение о переводе production.
SECTION 08 Что выбрать для следующего этапа
Если вам нужно сохранить стабильную производственную линию и одновременно подготовить Swift 6.4, отдельный удалённый Mac-узел удобнее, чем смешивать два toolchain в одном непредсказуемом рабочем каталоге. На странице тарифов VPSNIX можно оценить вариант аренды среды для временной проверки, но решение стоит принимать после проверки ваших требований к постоянной нагрузке, физическим интерфейсам, секретам подписи и времени работы.
Локальный Mac лучше подходит, если вам нужны физические устройства, USB-доступ или длительная неизменная нагрузка под вашим контролем. Самостоятельно купленный Mac mini рациональнее при постоянной эксплуатации и наличии команды, готовой обслуживать питание, сеть, обновления и резервное восстановление. Текущий удалённый CI-узел, напротив, может оказаться слабым вариантом, если в нём смешаны кэши, версии Xcode и ручные SDK-пути: это повышает стоимость диагностики и делает результат зависимым от состояния конкретной машины.
Если вам нужен временный изолированный стенд для двойной проверки Swift 6.4, а не ещё один постоянно перегруженный сервер, аренда Mac через VPSNIX позволяет отделить экспериментальную линию от production без немедленной покупки оборудования. Перед запуском зафиксируйте commit, lock-файл, параметры сборки и критерии возврата — тогда решение о миграции будет основано на повторяемых логах, а не на единичном удачном запуске.
Последнее обновление: 8 сентября 2026 года. Данные сверены с заметками к Xcode 27, системными требованиями Xcode и документацией Swift/Clang по модулям; статус beta необходимо повторно проверить при выходе RC или финальной версии.