Pull to refresh

Кросс-компиляция в Go

Reading time9 min
Views104K
Несмотря на то, что кроссплатформенность стала фактически стандартным атрибутом практически всех современных языков и библиотек, создавать по-настоящему кроссплатформенный продукт, всё равно было непросто. Компилируемые языки и сопутствующие библиотеки требовали сложной установки и настройки среды сборки и библиотек, а интерпретируемые — обязывали иметь или деплоить в составе необходимую версию интерпретатора. Есть немало проектов, пытающихся сделать этот процесс чуть более простым, но зачастую единственным решением оставалось устанавливать отдельный сервер и компилировать нативно.

В 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-go
blog.hashbangbash.com/2014/04/linking-golang-statically
www.limitlessfx.com/cross-compile-golang-app-for-windows-from-linux.html
Tags:
Hubs:
+49
Comments13

Articles