Собираем свой chromium/electron, с блекджеком и поэтессами

image

Добрый день. Меня зовут Тимур и я программист.

Сегодня я предлагаю посмотреть как можно подрихтовать исходники chromium-а, собрать свой вариант браузера и подтянуть это добро в electron. Эта статья — пробный шар, какая то часть ее позже перекочует в документацию проекта который, я надеюсь, смогу раскачать и сделать популярным, но об этом потом.

Если Вам хочется похейтить пользователей электрона в частности или джисеров в целом — проходите мимо. Электрон я сам не особо люблю и как раз пытаюсь сделать лучшую замену ему, что касается фронтовиков — я один из них, но про js в статье будет очень мало. Сорян.
Все телодвижения описанные ниже делались на маке, но я не трогал платформно зависимый код, думаю что под другими платформами соберется. Тем не менее, если вы захотите повторить под никсами — готовьтесь к тому что возможно придется немного править код. Если захотите повторить под виндой — то скорее всего править придется больше. Но если вы это сделаете и завернете все в pr — респект и уважуха ждут вас.

Идея вокруг которой крутится эта статья (и надеюсь многие другие, которые последуют) — простая до безобразия. Примерно как идея ноды — »а давайте прикрутим к v8 свой эвент луп и будем писать сервера на js». Хорошая идея, число хейтеров подтверждает. Или идея — »а давайте прикрутим спереди браузер, сзади — ноду и будем писать на этом кросс-платформенные приложения для десктопов». Это очень хорошая идея, число хейтеров подтверждает.

Моя идея хорошо вписывается в этот строй — а давайте выкинем все лишнее из хромиума, притащим этот огрызок в электрон, выкинем все лишнее из электрона что бы от него (электрона) ничего не осталось, да так что бы хейтерам было нечего ненавидеть и что бы они ненавидели нас за это. Хм. Хорошая идея, чем дольше над ней думаю тем больше мне она нравится.

Проблема каждой хорошей идеи — реализация. Естественно сразу после того как мне пришла в голову эта идея я с шашкой наголо бросился на исходники хромиума. Гм. Хромиум не просто большой. Он огромен. Он огромен как операционная система. Потому что он и является операционной системой. А последнее время он даже этого не скрывает и трасформируется в Chromium OS. Так вот, он огромен настолько что даже выкидывать код из него — не так просто как может показаться на первый взгляд. Но на второй взгляд и с нанадцатой попытки на самом деле можно освоить и это. Так что давайте возьмем себя в руки, уймем дрожь в коленках и приступим наконец уже.

Саму идею мы конечно реализовывать не будем, статья о гораздо более маленьком шаге — внести минимальные правки в chromium, собрать его, подтянуть это добро в electron и собрать электрон. Все. Но по пути я буду периодически отвлекаться от того что мы делаем на то зачем (в будущем) это нужно.

Часть моих размышлений была такой. Нода в электроне нужна для
— доступа в сеть (http клиент без песочницы и http сервер)
— доступа к фаловой системе (fs)
— доступа к шеллу os (exec)

В приниципе если мы это пробросим в chromium то и нода для electron не нужна и ее можно выкинуть. Ок.

Я решил начать с http клиента. В chromium-е есть такой — XMLHttpRequest или более модный fetch (под капотом у обоих с определенной глубины один и тот же код), но они работают в очень маленькой и тщательно наблюдаемой песочнице. Есть какое-то множество заголовков которые мы не можем выставлять, есть cors политики которые решают что мы можем, а что мы не можем делать, есть http авторизация в которую мы не можем залезть и все такое. Что если устроить XMLHttpRequest jailbreak? Освободить его, отпустить на свободу, звучит же?

Мысль была простой — насколько возможно одиночке разобраться в сорцах хромиума, выкинуть что то ненужное и при этом не поломать что то нужное. И я пошел проверять эту гипотезу.

Давайте приступим. Для начала давайте убедимся что мы можем собрать chromium с сорцов. Тут никакого rocket science, все делаем по инструкции. Я собирал под мак поэтому пользовался этой инструкцией. Если вы под другой осью то стоит начать отсюда.

Не буду пересказывать содержимое этого документа, если мы собираем с оригинальной репы без переключений веток то все должно пройти гладко. Единственное что могу добавить — если вы ставили/обновляли XCode то после этого стоит его еще и запустить — он что то у себя там доставляет при старте. У меня был момент когда до этого телодвижения сборка не собиралась, после собралась (что то то ли с либами то ли с хедерами) и это было не xcodebuild -license (см. в самом конце упомянутой инструкции)

Чего скромно не сказано в инструкции — вам потребуется порядка 50 гигов свободного места на винте. А поскольку мы после этого соберем еще и электрон — то это еще порядка 50 гигов. И если вы хотите не тупо копировать результат, а еще и поучаствовать в процессе — то оба куска по 50 гигов должны быть свободны одновременно, то есть около 100 гигов, иначе вам придется сначала править репу хромимума, комиттить, выливать куда либо (например гитхаб), сносить, качать репу электрона, собирать его и так далее. У меня сейчас порядка 110 гигов свободного места и система иногда жалуется что пора бы выкинуть что либо ненужное (во время сборок), репозитории я не сношу, но бывает что приходится удалять сборку хромимума во время сборки электрона (там что то порядка 6 гигов в директории сборки набегает). При желании можно извернуться и делать все только в репе электрона, но я сторонник тупых и простых решений.

После того как вы установили depots_tools вся последовательность действий сводится к:

mkdir chromium && cd chromium
fetch chromium
cd src
gn gen out/Default


На этом этапе у нас создана директория сборки и сгенерены конфиги сборки. В последней команде под виндой скорее всего будет еще что то про Visual Studio.
Теперь нам надо подправить out/Default/args.gn, делаем vim out/Default/args.gn и приводим его к виду:

# Set build arguments here. See `gn help buildargs`.
is_debug = false
is_component_build = true
symbol_level = 0


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

autoninja -C out/Default chrome


и дождаться завершения сборки.

Ок. Представим что вам повезло и все у вас собралось. Конгратсы! Давайте теперь что нибудь понаотключаем. Когда я писал эту статью была мысль начать с cookies, но это настолько большая тема (и не завершенная еще мной) что в итоге пришлось переключиться на что то более простое. Посмотрев критичным взглядом на то немногое что уже удалось сделать, волевым решением я выбрал forbidden headers (вот эти самые) которые нам не разрешают устанавливать программно. Давайте попробуем убрать это ограничение. И начнем это с самых знаменитых особей — user-agent и referer. Причем user-agent по спеке не является запрещенным хидером, но chromium продолжает думать за нас. Вот issue, на секунду 2015 года, но воз и ныне там.

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

Так вот. Маленький кусочек большой картины. Если мы разматываем что-то, что проброшено в js, то есть какая то сущность к которой мы из js имеем доступ, то скорее всего это что-то прикручено в blink и проброшено в v8. Blink — это рендерер, то что занимается отрисовкой DOM на вашем экране, v8 — это js движок. У вменяемых программистов конечно же возникает картинка в которой blink сидит отдельно, имеет доступ к DOM (и CSSOM — тут, немножко тут, CSS Typed Object Model) и занимается отрисовкой, v8 занимается вычислениями и все такое. Так вот. Нет. Код писали не благородные лесные эльфы-крестоностцы, как можно было бы ожидать, а вполне себе обычные люди, в трудных ситуациях идущие на компромиссы. А с учетом размеров проекта — идти на компромиссы приходилось часто, так что будьте готовы удивляться. Сам по себе код обычно легко читаем, надо отдать должное. Но вот архитектурные решения и размазанность этого кода по проекту первое время удивляют.

Ок. Возвращаемся к blink. Все что пробрасывается в js (а XMLHttpRequest — это не js, сомневающиеся могут открыть стандарт и попробовать его найти в нем — вот тут, версию выбирайте на свой вкус), так вот, все что пробрасывается в js — делается в обвязке v8 через IDL. IDL это такой язык описания интерфейсов, в хромиуме на основании IDL генерится какой то код обвязки, подробности можно начать добывать отсюда. Но на данном этапе нам достаточно понимания того что нам нужен blink.

Blink у нас лежит в third_party директории и у неискушенного читателя может сложиться впечатление что он автономен и выступает в роли зависимости, что то вроде npm-пакета в node_modules, но нет. Blink является практически неотъемлимой частью chromium, многое знает об его устройстве и не стесняется делать импорты из chromium-а что для порядочной зависимости является абсурдом. Не будем умничать, тут так принято (хотя да, и эти люди запрещают мне ковыряться в …). Что бы понять насколько blink прирос мясом к chromium можно взглянуть например сюда или сюда.

Ок. XMLHttpRequest. Лежит в /third_party/blink/renderer/core/xmlhttprequest. Видим в этой директории idl файл, открываем его и находим

[RaisesException] void setRequestHeader(ByteString name, ByteString value);


Отлично. Значит такой же setRequestHeader должен быть и в xml_http_request.cc. Сходим туда и находим XMLHttpRequest: setRequestHeader (вообще это позор что на хабре нельзя вставлять код с github-а)
В коде функции находим заветное if (cors::IsForbiddenHeaderName(name)) .... Отлично. Кто такой cors в этом контексте? Счастливые пользователи Clion обладающие нечеловеческим терпением и настроившие ide по этой инструкции ответят на это какой нибудь магической комбинацией кнопок, ну, а мы, простые колхозники, посмотрим просто в список импортов и зацепимся взглядом за 

#include "third_party/blink/renderer/platform/loader/cors/cors.h"


(by the way, тут сидит народ из JetBrains, вопрос к ним —, а есть какой то рецепт завести chroimium в Clion на обычной машинке, а не выкованной высшими эльфами из Гандолина? И что бы два раза не вставать —, а он mojom пережевывает?)
Ок. Идем в third_party/blink/renderer/platform/loader/cors/ и в cors.cc видим

bool IsForbiddenHeaderName(const String& name) {
  return !net::HttpUtil::IsSafeHeader(name.Latin1());
}


По пути замечаем что почти весь cors.cc blink-а это обвязка вокруг network::cors, удивляемся, но молчим. Тем более что конкретно в этом случае нам нужен не cors, а net::HttpUtil
net::HttpUtil мы находим в /net/http (тут кстати уместно упомянуть Getting Around the Chromium Source Code Directory Structure — стоит ознакомиться для старта)
Открываем http_util.cc и вот оно, самое мясо! Отлично! Нам нужно только закомментировать «referer» и «user-agent». (обратите внимание на коммент над user-agent, вообще чтение комментариев в сорцах хромимума частенько дает интересную информацию). Ок. Теперь функция HttpUtil::IsSafeHeader будет заглядывать в список kForbiddenHeaderFields, НЕ будет там находить «referer» и «user-agent» и мы сможем их выставлять программно! Booyah!!!

Собираем по новой:

autoninja -C out/Default chrome


В этот раз все пройдет гораздо быстрее, вместо 50 000 артефактов собираться будет пара тысяч (сейчас точно не помню сколько было конкретно в этом случае, но обычно если mojom не трогали и никого не выкидывали [функции/методы/классы] редко более 5–7 тысяч артефактов пересобирается).
Запускаем chromium и скармливаем ему какой нибудь html типа


  
    
  












И дрожащей от нетерпения рукой открываем консоль в браузере. Ну что, обломались? Я вот тоже так же обломался. Ладно, не буду томить, вот коммит который делает работу. Тут кстати прикольная фича, я могу показать этот же коммит, но в репе chromium-а, вот смотрите, не уверен как к этому относиться так что пофигу. Но сдается мне что король голый, а форк не настоящий, но это потом, на тему автономности я планирую отдельную статью.

Последовательность действий которая привела к решению была простая — сначала я нашел net::HttpRequestHeaders::kUserAgent, грепнул по нему /content, /net, /services/network и /third_party/blink что дало мне request.SetReferrerString, SetHeader, RemoveHeader, GetHeader и SetHeaderIfMissing который вывел на ComputeUserAgentValue

Ну, а поскольку интрига сорвана давайте уже воспользуемся готовым результатом.
Естественно я уже выложил все в репе, вот тут, нам нужна ветка without/restricted-headers
Не торопитесь делать git clone. Если вы собирали хромиум по инструкции то сама репа у вас уже есть, она лежит в chromium/src. Для того что бы пролить в нее ветки с моей репы достаточно сделать

git remote add gonzazoid https://github.com/gonzazoid/chromium.git


после этого делаем git remote -v и убеждаемся что форк прописан в репе, должно быть что то вроде:

~/Documents/chromium/src λ git remote -v
gonzazoid	https://github.com/gonzazoid/chromium.git (fetch)
gonzazoid	https://github.com/gonzazoid/chromium.git (push)
origin	https://chromium.googlesource.com/chromium/src.git (fetch)
origin	https://chromium.googlesource.com/chromium/src.git (push)

Отлично. Git прекрасный инструмент для распределенной работы и мы еще поговорим об этом позже. А пока что нас интересует ветка without/restricted-headers.
Делаем git fetch gonzazoid, это прольет в локальную репу все изменения форка. После переключимся в нужную ветку: git checkout without/restricted-headers. Либо можно сделать git pull в текущую ветку, но тут есть ньюансы.

Если вы собирали с main то собирали вы скорее всего другую версию хромиума, не ту с которой отбранчевалась ветка without/restricted-headers. А значит (с большой вероятностью) сборка не заработает. Потому что нужно:

  • удалить директорию сборки
  • по новой сделать gclient sync
  • по новой сгенерить билд директорию gn gen out/Default
  • поправить args.gn в ней


и только после этого выпить по новой пару литров кофе запустить по новой сборку:
autoninja -C out/Default chrome

Это долго и нудно. Но это стоит держать в голове. Однако когда вы переключаетесь между ветками которые отбранчевались с одного и того же коммита после переключения не только не надо делать gclient sync, но и не надо трогать директорию сборки. Нинзя сам разберется какие файлы поменялись, пересоберет объектники и все такое.

Ок, переключились на ветку without/restricted-headers и предположим что собрали ее. Скармливаем вышеупомянутый html и видим в консоли что то вроде:

image
Да, мы можем выставлять эти хедеры и хромимум даже бровью не ведет. Чудненько.

Но как браузер такая сборка не особа полезна (хромиум как браузер вообще не особо, частенько oh-snap-ает, на видео хостингах вообще бесполезен из за своего набора кодеков и все такое)
Но вот если мы это притащим в электрон, то там это может пригодиться.

Ок. Собираем электрон. (IRL вы еще захотите сохранить свои изменения, запушить их и повесить на коммит тег, но так как это учебная статья и я все уже сделал — то этот момент пока опускаем)

Знакомство со сборкой электрона стоит начать отсюда. После сборки chromium-а там почти все для вас будет знакомо. В сухом остатке:

mkdir electron && cd electron
gclient config --name "src/electron" --unmanaged https://github.com/electron/electron
gclient sync --with_branch_heads --with_tags
cd src
export CHROMIUM_BUILDTOOLS_PATH=`pwd`/buildtools
gn gen out/Testing --args="import(\"//electron/build/args/testing.gn\") $GN_EXTRA_ARGS"

Тут обратите внимание на строку

export CHROMIUM_BUILDTOOLS_PATH=`pwd`/buildtools


Если вы после сборки электрона в этой же сессии терминала пойдете собирать chromium то CHROMIUM_BUILDTOOLS_PATH будет указывать на build tools электрона. А когда вы снесете электрон из за того что у вас не хватает места то CHROMIUM_BUILDTOOLS_PATH будет указывать в никуда. Соотв. к сборке chromium-a (если вы решили вдруг пересобрать его ПОСЛЕ сборки электрона) добавляется этот же шаг — перед генерацией сборки делаем

export CHROMIUM_BUILDTOOLS_PATH=`pwd`/buildtools


в директории сорцов chromium-а.
Вернемся к электрону.
Структура директорий будет такая же как и у chromium, потому что в src у вас сейчас лежит на самом деле хромиум. Просто в его код добавилась директория electron в которой и лежит репа электрона. Ок. Запускаем сборку:

ninja -C out/Testing electron


Опять же, если мы не чудили и все делали по инструкции то скорее всего все соберется. А вот как бы нам теперь электрону подсунуть свой chromium?
Побродив по сорцам находим в корне проекта /DEPS файл. И видим в нем строки:

  'chromium_version':
    '98.0.4706.0',
  'node_version':
    'v16.13.1',


Скорее всего нам сюда. Дальше в этом же файле видим:

'chromium_git': 'https://chromium.googlesource.com',
'electron_git': 'https://github.com/electron',
'nodejs_git': 'https://github.com/nodejs',


Это мы удачно зашли.
Смотрим как это используется (в этом же файле)

deps = {
  'src': {
    'url': (Var("chromium_git")) + '/chromium/src.git@' + (Var("chromium_version")),
    'condition': 'checkout_chromium and process_deps',
  },


Тут встает вопрос —, а что менять? Если мы поменяем chromium_git то строка src.url соберется неправильно. Дело в том что у гугля соглашение https://chromium.googlesource.com/имя проекта/src.git, а у гитхаба https://github.com/имя пользователя/[имя проекта].git. Думаем, выходим из себя, бунтуем и меняем:

'chromium_git': 'https://chromium.googlesource.com/chromium/src.git@'
  'src': {
    'url': (Var("chromium_git")) + (Var("chromium_version")),


остается только прописать тег который мы повесили на наш коммит в chromium:

vars = {
  'chromium_version':
    'ur.xhr.98.0.4706.0',

Тут небольшое отступление. Версия chromium-а важна. Электрон пользуется chromium-ом не через Chromium Embedded, а довольно агрессивно лезет в потроха и применяет свой набор патчей. Соответственно просто так взять и поменять версию chromium-а в сборке электрона как правило не получится — нужно танцевать от той сборки chromium-а на которую рассчитывает electron.

То есть в нашем случае нам нужна не последняя версия chromium в main, а конкретно 98.0.4706.0. Если вы повторяете самостоятельно все шаги в статье — не расстраивайтесь. Можно сделать git stash, отбранчеваться от нужного тега и сделать git stash apply. Либо, если вы уже закоммитили — отбранчеваться от нужного тега и сделать git cherry-pick нужного коммита. Я в свое время начал эксперименты с отключения cookie вот в этой ветке отбранчевавшись от main, и когда дело дошло до электрона пожалел о своей беспечности. Но cherry-pick и за два вечера перенес порядка 80 коммитов в нужную ветку, ничего страшного.

Ок. Вернемся к сборке электрона. Наши коммиты в chromium-е вылиты на github, потеганы, конфиг электрона поправлен и мы готовы собирать. Но мы поменяли версию chromium-а и надо бы обновить все дерево сорцов и перегенерить план сборки.
Вот тут у меня довольно не проработанная часть. Я на самом деле сделал форк electron-а, завел свою ветку, закоммитил в ней изменения, вылил на github и запустил сборку вот так:

gclient config --name "src/electron" --unmanaged https://github.com/gonzazoid/electron@feature/custom-chromium
gclient sync --with_branch_heads --with_tags
cd src
export CHROMIUM_BUILDTOOLS_PATH=`pwd`/buildtools
gn gen out/Testing --args="import(\"//electron/build/args/testing.gn\") $GN_EXTRA_ARGS"


Поскольку директория src/electron и есть репа электрона, наверняка то же самое можно сделать локально, без выливания изменений на github, в инструкции по сборке электрона даже описывают это, но я просто пока еще с этим не разбирался. Если кто пройдет этот путь короче — you are very welcome! Делитесь опытом, я обновлю статью.

Так. Сборка. Запускаем

ninja -C out/Testing electron


И довольно быстро падаем с ошибкой про LLVM. Почему? Потому что chromium_git используется не только электроном, но и самим chromium для сборки, он пытается сходить по тому урлу что мы ему дали и выкачать оттуда llvm (похоже что подрихтованный под этот проект иначе что бы ему не сходить по урлу самого LLVM), а так как там где мы ему указали LLVM нет — он падает.

Это очередной звоночек на тему автономности и опенсорсности, но пока просто запоминаем это и идем дальше.

Очевидно что нам придется оставить chromium_git в покое, а менять значение в самой сборке урла:

'url': 'https://github.com/gonzazoid/chromium.git@' + (Var("chromium_version")),


И вот теперь то оно наконец то соберется. Ну наконец то! Помните тот html что мы ваяли для проверки в chromium? Вспоминаем его путь и делаем:
.

/out/Testing/Electron.app/Contents/MacOS/Electron путь до html


Открываем консоль в электроне и видим ту же картину что и на скриншоте выше. Отлично!

В принципе все. Если у вас появилось ощущение что вас обманули, а вы рассчитывали на большее — вот тут есть ветка в которой я отключил http авторизацию, а вот тут — частично cookies. Тут отключен cors. Можно поиграться с ними. А под тегом ur.xhr.98.0.4706.0
на самом деле лежит сборка в которой слиты все упомянутые ветки. Если вы соберете electron с github.com/gonzazoid/electron@feature/custom-chromium то получите сборку в которой довольно многое отключено. Я сваял тесты для этой сборки, их можно найти тут.
Делаем (в отдельной сессии терминала)

git clone https://github.com/gonzazoid/check-unrestricted-xhr-electron
cd check-unrestricted-xhr-electron/test-server
nmp install
node server.js


После этого запускаем

./out/Testing/Electron.app/Contents/MacOS/Electron путь до index.html в check-unrestricted-xhr-electron


И увидим что то вроде:

image
Что то из этого уже работает, что то еще нет, все интересное только начинается!

Ну, а на пока — мы научились собирать chromium, немножко его править и подтягивать эти изменения в electron. Вполне себе результат сам по себе.

В следующей статье я намерен показать более серьезное вторжение в сорцы chromium-а, нам придется править немного и сорцы electron-а, рассмотрим немного коллбэки и структуры используемые в chromium.

Отдельной статьей будет объяснение того зачем я это все делаю и как я вижу как электрон можно сделать стройным как Снегурочку, но поскольку это уже будет «Я пиарюсь» — если вы хотите это продолжение — поддержите кармой.

Все. Новостей на сегодня больше нет, с вами был Тимур, хорошего настроения!

© Habrahabr.ru