Кросс-компиляция в 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-go
    blog.hashbangbash.com/2014/04/linking-golang-statically
    www.limitlessfx.com/cross-compile-golang-app-for-windows-from-linux.html
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 12
    • +2
      Можно вопрос не по теме топика, но по теме Go.

      Вопрос по организации кода.
      У меня на машине такая структура:
      Projects
      ---ASP.NET
         ---Proj1
         ---Proj2
      ---PHP
         ---Proj1
         ---Proj2
      ---GO
         --- ???
      

      Как должна быть организована структуру кода на Go? Я читал официальную и не официальную документацию.
      В Go Есть понятие Workspaces. Которые содержат в себе следующую структуру
      — src
      — pkg
      — bin
      Что это такое? Каждый Workspace это отдельный проект на Go и в каждом проекте должна быть структура как выше? Для каждого проекта на Go путь до него обязательно добавлять в $PATH?

      Такая банальная вещь, но в доках и других туториалах на этот вопрос нет очевидного ответа.
      Буду признателен за ответ.
      • +3
        Ну как же нет? golang.org/doc/code.html#Workspaces

        В 90% случаев вы один раз в жизни устанавливаете свой рабочий workspace — GOPATH=C:\Projects\GO (в вашем примере) и работаете в этой директории. К примеру, ваши проекты могут быть в:
        C:\Projects\GO\src\test\mytestprj\
        C:\Projects\GO\src\github.com\paco\mycoolprj\

        Можно писать код и вне GOPATH, но все сторонние пакеты которые вы будете использовать (и ставить через go get) — будут сохраняться именно в GOPATH\src и оттуда браться компилятором.

        В src хранятся исходные коды. В pkg — скомпилированные бинарные версии пакетов, которые предназначены для линковки (библиотеки, другими словами). В bin — скомпилированные файлы для исполнения (бинарники). $GOPATH/bin удобно добавить в PATH, потому как тогда легко инсталлировать внешние Go-программы (ту же go-bindata) одним телодвижением — go install — и оно готово к использованию в вашей системе.
        • 0
          Благодарю за ответ. Разбитие namespace по папкам внутри src конечно не привычно.
          • 0
            Непривычно?

            Это же классический java-style, а в PHP подобное поведение описывается в PSR4, не знаю правда что там с ASP.NET, но больее чем уверен что такая организация кода вполне возможна, и всякие гуру-папки активно ее используют, потому что не надо лишних слов — просто заставь имена пакетов и папок на диске кореллировать между собой.
      • 0
        Но тут возникает следующий момент — раз кросс-компиляция и деплой становятся такими простыми и быстрыми, появляется стимул все зависимости от файлов — будь-то конфиги, сертификаты или что угодно еще — встраивать в бинарник тоже

        +100500
        У Меня тоже возник такой соблазн
        Более того я это соблазну подался!!! И веб-морду (index.html) воткнул в виде зашифрованной в base64 строки
        const indexPageD = "PCFkb2N0eXBlIGh0bWw+DQo8aHRtbD4NCg0KPGhlYWQ+DQoJPHRpdGxlPldlYlRvcDwvdGl0bGU+DQoJP
        ......
        func (service *TopJsonService) ServePage(responseWriter http.ResponseWriter, request *http.Request) {
        	responseWriter.Header().Set("Content-Type: text/html", "*")
        	content, err := ioutil.ReadFile("index.html")
        	if err != nil {
        		val, _ := base64.StdEncoding.DecodeString(indexPageD)
        		responseWriter.Write(val)
        		return
        	}
        	responseWriter.Write(content)
        }
        

        github.com/Loafter/WebTop/blob/master/WebService.go

        Вообще очень полезные вы статии пишите.

        Кстати я не понимаю как еще полностью статически слинокованный дистрибутив линукс еще не вышел
        • +1
          Если вам нравится всё встраивать в бинарник, посмотрите проект go.rice, очень удобная штука :)
          Можно целые папки автоматически встраивать, а во время разработки — читать из файла.
          • 0
            JFYI ещё одна либа для встраивания всякой внешней статики в бинарник github.com/rakyll/statik
            Больше либ хороших и разных.
          • 0
            Спасибо)

            Ну, в base64 засовывать — это не очень продуктивно в общем случае. Лучше таки или go-bindata (+go-bindata-assetfs) или go-rice использовать.Тем более, что c go generate их еще удобнее стало использовать.
          • 0
            Может кто сможет заставить работать influxdb под windows? Смог скомпилировать, даже запускается, но не делает того что надо.
            github.com/influxdb/influxdb/blob/master/docs/contributing.md
            Тоже написано на Go. Но почему-то их команда упорно игнорирует windows.
            • 0
              InfluxDB — отличная вещь. А в чем там проблема? Не вижу открытых issue по теме windows…
            • 0
              Про GO386 забыли
              Иначе вроде i386 а на athlon xp без sse2 не запустится!
              В нете очень много i386 или x86 скомпилированных под >=sse2 и *i386*.deb в том числе.

              $GO386 (for 386 only, default is auto-detected if built on either 386 or amd64, 387 otherwise)
              This controls the code generated by gc to use either the 387 floating-point unit (set to 387) or SSE2 instructions (set to sse2) for floating point computations.

              GO386=387: use x87 for floating point operations; should support all x86 chips (Pentium MMX or later).
              GO386=sse2: use SSE2 for floating point operations; has better performance than 387, but only available on Pentium 4/Opteron/Athlon 64 or later.
              • 0
                SSE2 Не поддерживают:
                Поскольку SSE2 — расширение IA-32, процессоры, не поддерживающие IA-32, не поддерживают SSE2. Все известные процессоры x86-64 также поддерживают SSE2.

                Кроме того, не поддерживают IA-32-совместимые процессоры, появившиеся до SSE2:

                Все AMD до Athlon 64
                Все Intel до Pentium 4
                VIA C3
                Transmeta Crusoe

                PS т.е. i386 точно работает на x86-64.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.