Кросс-компиляция в Go
Несмотря на то, что кроссплатформенность стала фактически стандартным атрибутом практически всех современных языков и библиотек, создавать по-настоящему кроссплатформенный продукт, всё равно было непросто. Компилируемые языки и сопутствующие библиотеки требовали сложной установки и настройки среды сборки и библиотек, а интерпретируемые — обязывали иметь или деплоить в составе необходимую версию интерпретатора. Есть немало проектов, пытающихся сделать этот процесс чуть более простым, но зачастую единственным решением оставалось устанавливать отдельный сервер и компилировать нативно.В Go кросс-платформенность вышла на тот уровень, когда впервые можно смело отказаться от compile farms, специально настроенных dev-сред, виртуальных машин для сборки или chroot/docker-dev решений. И это ещё один серьезный game-changer, подробнее о котором я и хочу рассказать и показать на примерахПоехали.
Как известно, в Go сознательно отказались от динамической линковки — по ряду причин, основная из которых очень схожа с обычным объяснением дизайна почти любого аспекта Go — «преимущества [динамической линковки] намного меньше её недостатков и сложности, которая она привносит в архитектуру». Что ж, главной причиной появления dynamic linking было желание экономить ресурсы — прежде всего диcковое пространство и память — которые сейчас достаточно дешевы, не только на серверах, но и в embedded-устройствах (коптеры, к примеру, несут на борту уже по 1–2 Гб RAM!). Вобщем, перечислять плюсы и минусы отдельного способа линковки — это потянет на отдельный пост, так что пока просто принимаем, как есть — в Go на выходе всегда имеем статический бинарник.
На данный момент для актуальной версии Go 1.4.1 реализована поддержка следующих платформ:
Linux 2.61 и выше — amd64, 386, arm MacOS X 10.6 и выше — amd64, 386 Windows XP и выше — amd64, 386 FreeBSD 8 и выше — amd64, 386, arm NetBSD — amd64, 386, arm OpenBSD — amd64, 386 DragonFly BSD — amd64, 386 Plan 9 — amd64, 386 Google Native Client — amd64p32, 386 Android — arm 1 — официально поддерживаются ядра 2.6.23 и выше, но в реальности всё работает и на более ранних ядрах ветки 2.6 — к примеру немало людей используют Go на RHEL5/CentOS5 с 2.6.18.В Go 1.5 ожидается поддержка iOS.Еще примечательно, что изначально поддержки Windows в Go не было — команда маленькая, и пачкать руки заниматься имплементацией кода для Windows было некому, но благодаря тому, что проект открыли для open-source разработки — порт для Windows был очень быстро написан сторонними людьми и интегрирован в официальную кодовую базу.
Хотя описанные далее процессы будут абсолютно одинаковы для всех платформ (за исключеним, разве что, Android и Native Client (NaCl), для которых нужны лишние телодвижения), далее в статье будет по-умолчанию считаться, что вы используете одну из трех самых популярных десктопных платформ — Linux, MacOS X или Windows. Кроме того, для большей простоты я буду подразумевать, что мы пишем и используем исключительно Go-код, без необходимости линковаться с С-библиотеками (и, тем самым, без необходимости использовать cgo/gcc). Есть еще отдельный кейс — когда нужно использовать ряд функций из стандартной библиотеки, завязанных на cgo, но об этом я напишу отдельной главой в конце.
Подготовка toolchainПервый шаг, который необходимо выполнить — это собрать toolchain для нужной платформы.Переходим в директорию с исходным кодом Go (она же $GOROOT/src, она же всегда есть у вас на машине) и пересобираем под нужную платформу, скажем Windows/amd64:
cd $(go env GOROOT)/src sudo GOOS=windows GOARCH=amd64 CGO_ENABLED=0 ./make.bash --no-clean Процесс занимает на Macbook Air 2012 около 26 секунд. Скрипт make.bash — это стандартный скрипт сборки Go, которым бы вы инсталлировали Go, если бы ставили из исходников. Он собирает, собственно, Go, и всю стандартную библиотеку, только в этот раз — для платформы windows/amd64.Также, по упомянутой выше причине, мы отключили поддержку CGO.Значения GOOS и GOARCH Таблица значений GOOS (если кто знает, как на Хабре сделать таблица в 50% ширины — подскажите): OS $GOOS Linux linux MacOS X darwin Windows windows FreeBSD freebsd NetBSD netbsd OpenBSD openbsd DragonFly BSD dragonfly Plan 9 plan9 Native Client nacl Android android И GOARCH:
Architecture $GOARCH x386 386 AMD64 amd64 AMD64 с 32-указателями amd64p32 ARM arm Пример 1. Веб-сервер, написанный и собранный в Linux для Windows Напишем простенький веб-сервер, который в Go писать проще, чем в некоторых языках/библиотеках парсить командную строку. package main
import ( «log» «net/http» )
func Handler (w http.ResponseWriter, r *http.Request) { w.Write ([]byte («Hello, world\n»)) }
func main () { http.HandleFunc (»/», Handler)
log.Println («Starting HTTP server on:1234») log.Fatal (http.ListenAndServe (»:1234», nil)) } И соберем его для Windows 32- и 64-bit: GOOS=windows GOARCH=386 go build -o http_example.exe GOOS=windows GOARCH=amd64 go build -o http_example64.exe Проверяем: $ file http_example*.exe http_example.exe: PE32 executable for MS Windows (console) Intel 80386 32-bit http_example64.exe: PE32+ executable for MS Windows (console) Mono/.Net assembly Думаю, не нужно говорить, что оба бинарника готовы к копированию на целевую Windows-систему и будут работать.
Пример 2. Кросс-компиляция под ARM для телефона Nokia N9 Сразу скажу, что сейчас я с embedded-девайсами плотно не работаю, поэтому могу какие-то детали не знать — так что постараюсь не углубляться в эту тему, но в целом за ситуацией с Go на embedded слежу. Вообще говоря, Go не позиционировался как язык для embedded-платформ, что, впрочем, не помешало народу активно начать его использовать в этой области. Возможно, причина в том, что embedded-индустрия сделала скачок вперед, и теперь «встраиваемое» устройство уже не означает критически малое количество ресурсов, а возможно компромиссы не в пользу экономии памяти в Go оказались гораздо менее ощутимыми на практике, но факт есть факт — для Go уже создано масса проектов вроде Gobot (robotics-фреймворк для целой кучи платформ — от Arduino, Raspberry PI и Beaglebone Back до LeapMotion, Pebble и ArDrone) или EMBD (фреймворк для работы с hobby-бордами), а PayPal уже пару лет использует Go в своем beacon-девайсе для беспроводных чекинов и платежей.Для примера возьмем Nokia N9 (или N950, кому повезло) — и соберем вышеприведенный пример для него:
GOOS=linux GOARCH=arm go build -o http_example_arm scp http_example_arm developer@192.168.2.16:/home/user/
Вот так просто, да.
Для ARM-платформ, на самом деле, может понадобиться еще указывать флаг GOARM, но тут, если версия по-умолчанию не подходит, бинарник на целевой платформе выдаст понятное сообщение, вроде такого:
runtime: this CPU has no floating point hardware, so it cannot run this GOARM=7 binary. Recompile using GOARM=5. Автоматизируем процесс Казалось бы, что может быть проще указания одной переменной перед go build. Но есть ситуации, когда код нужно собирать и деплоить на разные платформы по 100 раз в день. Для таких задач есть несколько проектов, для автоматизации процессов подготовки toolchain-ов и, непосредственно, сборки кода под нужную платформу.Gox Ссылка: github.com/mitchellh/goxИнсталляция и подготовка сразу всех возможных toolchain-ов: go get github.com/mitchellh/gox gox -build-toolchain … Теперь, вместо «go build», пишем «gox»:
$ gox Number of parallel builds: 4
--> darwin/386: github.com/mitchellh/gox --> darwin/amd64: github.com/mitchellh/gox --> linux/386: github.com/mitchellh/gox --> linux/amd64: github.com/mitchellh/gox --> linux/arm: github.com/mitchellh/gox --> freebsd/386: github.com/mitchellh/gox --> freebsd/amd64: github.com/mitchellh/gox --> openbsd/386: github.com/mitchellh/gox --> openbsd/amd64: github.com/mitchellh/gox --> windows/386: github.com/mitchellh/gox --> windows/amd64: github.com/mitchellh/gox --> freebsd/arm: github.com/mitchellh/gox --> netbsd/386: github.com/mitchellh/gox --> netbsd/amd64: github.com/mitchellh/gox --> netbsd/arm: github.com/mitchellh/gox --> plan9/386: github.com/mitchellh/gox
Можно указывать конкретный пакет или конкретную платформу:
gox -os=«linux» gox -osarch=«linux/amd64» gox github.com/divan/gorilla-xmlrpc/xml Остальные аргументы командной строки идентичны go build. Достаточно интуитивно.GoCX GoCX — это один из самых известных врапперов вокруг фич кросс-компиляции, но с упором на пакаджинг (умеет делать .deb даже) и различные плюшки для автоматизированных сборок. Сам не пользовал, поэтому, кому интересно, смотрите сайт и документацию.github.com/laher/goxcРазбираемся с CGO Если кто-то смотрел видео с конференции GopherCon 2014, которая проходила прошлой весной в Денвере, то, возможно, помнит выступление Alan Shreve «Build Your Developer Tools in Go» — и одну из вещей, которую он говорит достаточно категорично: «не используйте кросс-компиляцию, компилируйте нативно». Дальше идет объяснение — причина в Cgo. Если вам не нужно использовать cgo — все окей. И на самом деле, очень малая часть очень специфичного кода в Go нуждается в сторонних С-библиотеках. В чем же проблема? Проблема в том, что некоторые функции стандартной библиотеки зависят от cgo. Тоесть, если мы собираем Go с CGO_ENABLED=0, они просто не будут доступны и на этапе компиляции мы получим ошибку. Несмотря на то, что тут есть очень удобный и красивый workaround, давайте разберемся, что же именно в стандартной библиотеке зависит от cgo.
К счастью, сделать это просто:
# cd $(go env GOROOT)/src/ # grep -re »^// +build.*[^\!]cgo» * crypto/x509/root_cgo_darwin.go:// +build cgo net/cgo_android.go:// +build cgo,! netgo net/cgo_linux.go:// +build! android, cgo,! netgo net/cgo_netbsd.go:// +build cgo,! netgo net/cgo_openbsd.go:// +build cgo,! netgo net/cgo_unix_test.go:// +build cgo,! netgo os/user/lookup_unix.go:// +build cgo runtime/crash_cgo_test.go:// +build cgo Вкратце пройдемся по этим файлам:
crypto/x509/root_cgo_darwin.go — имплементирует одну функцию для получения корневых X.509 сертификатов в MacOS X. Если вы не используете явно эту фичу — ничего страшного, без cgo у вас все будет работать. net/cgo_android/linux/netbsd/openbsd/cgo_unix_test.go — код необходимый для использования нативного DNS-резолвера в разных unix-ах. Чуть ниже подробности. os/user/lookup_unix.go — функции из пакета os/user — для получения информации о текущем юзере (uid, gid, username). Используется getpwuid_r () для чтения passwd-записей runtime/crash_cgo_test.go — файл с тестами для хендлинга крешей, ничего релевантного Теперь подробнее про DNS-resolver.Каждый файл из того списка (который скомпилируется только для своей платформы благодаря тегам // +build) содержит имплементацию единственной функции cgoAddrInfoFlags (), которая, в свою очередь, используется в cgoLookupIP (), которая, используется в dnsclient_unix.go, в котором мы находим функцию goLookupIP (), которая служит fallback-вариантом при отсутствии cgo-enabled кода, и тут же находим объяснение: // goLookupIP is the native Go implementation of LookupIP.// Used only if cgoLookupIP refuses to handle the request// (that is, only if cgoLookupIP is the stub in cgo_stub.go).// Normally we let cgo use the C library resolver instead of// depending on our lookup code, so that Go and C get the same// answers.
goLookupIP фактически резолвит только по Hosts-файлу и по DNS-протоколу, что для большинства систем — ок. Но могут быть проблемы, если в системе будут использоваться нестандартные методы резолвинга имён. Полагаю, что в 99% случаев, hosts и dns будут более, чем достаточно.
В сухом остатке имеем — если ваш код не использует С/С++-библиотеки через Cgo, и не использует следующие две вещи:
проверку x.509 сертификатов, которая должна работать на MacOS X гарантированно получать системную информацию о текущем юзере то на все заморочки с Cgo можно забить.Первая часть (с X.509) на самом деле не такая уж редкая. Если я правильно понимаю — этот код нужен, если ваша программа использует стандартный net/http.StartAndListenTLS () — и вы используете реальные сертификаты, которые реально нужно проверять.
Поэтому вкратце о простом workaround вокруг этой темы — называется он gonative, и делает одну простую вещь — скачивает с официального сайта бинарные версии golang нужной версии для нужной платформы, в которой уже есть скомпилированные бинарники всех стандартных пакетов и, фактически, завершает процесс «собрать toolchain с cgo-кодом».Всё что нужно сделать, это установить её (go get github.com/inconshreveable/gonative) и выполнить одну простую команду:
gonative И дальше использовать стандартные процедуры кросскомпиляции, как и раньше, ручками или через gox/gocx.Подробнее о gonative тут: inconshreveable.com/04–30–2014/cross-compiling-golang-programs-with-native-libraries/Практическое применение Теперь о главном — применении на практике. Я использовал в продакшене пока только три схемы — «сборка на darwin/amd64 → деплой на linux/386», «linux/amd64 → linux/386» и «linux/amd64 → windows/amd64». Это продукты, которые уже больше года полноценно работают. Третий случай (деплой на windows) тогда меня вообще застал врасплох — был сервер, успешно бегущий на Linux, и тут вдруг резко понадобилось его запускать на Windows. Причем «вот срочно надо». Вспоминая бессонные ночи опыта с кросс- — да что там кросс, просто с компиляцией Qt для деплоя на Windows — 60-секундный процесс «гуглим как это делается → сборка toolchain → перекомпиляция проекта → деплой на windows» — стал просто шоком, я тогда даже не поверил глазам.Но тут возникает следующий момент — раз кросс-компиляция и деплой становятся такими простыми и быстрыми, появляется стимул все зависимости от файлов — будь-то конфиги, сертификаты или что угодно еще — встраивать в бинарник тоже. Впрочем, это достаточно простая задача, даже для сторонних библиотек, благодаря эффективному использованию io.Reader интерфейса и пакету go-bindata, но это уже тема для отдельной статьи.
Надеюсь, ничего из главного не упустил.Но в целом это на самом деле очень существенная разница со всем предыдущим опытом кросс-сборки. Если честно, я до сих пор не привык к этой перемене. Больше не нужны виртуалки с настроенной dev-средой, не нужны докер-имиджи для сборки — да вообще dev-environment отпадает как таковой. Это слишком резкий game changer, чтобы так быстро привыкнуть. :)
Ссылки dave.cheney.net/2012/09/08/an-introduction-to-cross-compilation-with-goblog.hashbangbash.com/2014/04/linking-golang-statically/www.limitlessfx.com/cross-compile-golang-app-for-windows-from-linux.html