No começo, eu queria só atualizar uma lib: Atualizando o SASS do Computaria. Então, tudo começou a desandar… Quebrei o CSS com a publicação anterior, e agora?.

E eu saí com uma vitória amargurada, colocando para fazer o build do sistema via uma imagem Debian, e não sobre o Alpine.

A morte do rei

– Envenenado, ele foi envenenado.

O legista, sobre a causa da morte

Foi uma sucessão de erros que gerou o problema no final. Para começar, eu não havia levantado o Gemfile.lock com as mudanças locais. Fui fazer isso apenas em um momento posterior.

Depois, eu percebi que não havia aplicado o freeze. Minha primeira solução foi inserir manualmente, mas um bundle update ou bundle install logo em seguida iria remover as linhas inseridas manualmente. Isso não é bom, porque o Gemfile.lock ficaria distinto do desejado. Como corrigir isso?

Bem, vamos olhar o final do lock:

PLATFORMS
  arm64-darwin-22
  x64-mingw32
  x86_64-linux

DEPENDENCIES
  cli-ui
  dotenv (~> 3.1)
  duktape (~> 2.6.0.0)
  execjs (~> 2.8.1)
  jekyll (= 4.3.3)
  jekyll-katex
  tzinfo-data
  webrick

RUBY VERSION
   ruby 3.2.1p31

BUNDLED WITH
   2.5.11

Olha que legal! Uma seção chamada PLATFORMS! Tem outra chamada DEPENDENCIES, que contém as dependências explicitamente citadas no Gemfile. Ok, PLATFORMS não tá no Gemfile em si. Então, será que consigo adicionar uma plataforma no lock? Depois de pesquisar em alguns lugares, encontrei:

bundle lock --add-platform x86_64-linux-musl

Isso foi um dos pontos que causou problema ao atualizar, um dos pontos que envenenou rodar no Alpine. Mas, tem mais?

Bem, sim. No final das contas, a atualização do google-protobuf teve um efeito colateral: aparentemente ele tenta buscar a gem com extensões nativas. E por que atualizamos o google-protobuf? Por conta da atualização do SASS.

Para atualizar o SASS, precisamos atualizar o sass-embedded:

-    google-protobuf (4.26.1)
+    google-protobuf (4.29.3)
+      bigdecimal
+      rake (>= 13)
+    google-protobuf (4.29.3-arm64-darwin)
+      bigdecimal
+      rake (>= 13)
+    google-protobuf (4.29.3-x86_64-linux)
+      bigdecimal
       rake (>= 13)

 ...

-    sass-embedded (1.77.2-arm64-darwin)
-      google-protobuf (>= 3.25, < 5.0)
-    sass-embedded (1.77.2-x64-mingw32)
-      google-protobuf (>= 3.25, < 5.0)
-    sass-embedded (1.77.2-x86_64-linux-gnu)
-      google-protobuf (>= 3.25, < 5.0)
-    strscan (3.1.0)
+    sass-embedded (1.83.4-arm64-darwin)
+      google-protobuf (~> 4.29)
+    sass-embedded (1.83.4-x64-mingw32)
+      google-protobuf (~> 4.29)
+    sass-embedded (1.83.4-x86_64-linux-gnu)
+      google-protobuf (~> 4.29)

E não há, explicitamente, nenhuma dependência relativa ao sass nem ao google-protobuf. Tudo isso é gem que foi atualizada e compatível com as restrições colocadas de version range, gems que são dependências transitivas das dependências listadas explicitamente:

DEPENDENCIES
  cli-ui
  dotenv (~> 3.1)
  duktape (~> 2.6.0.0)
  execjs (~> 2.8.1)
  jekyll (= 4.3.3)
  jekyll-katex
  tzinfo-data
  webrick

Mas, afinal, qual foi o problema da extensão nativa do google-protobuf? Bem, vamos lá. temos aqui esses comentários em issue que me ajudaram a chegar on diagnóstico final:

Basicamente, o build para do google-protobuf com extensões nativas que é aarch64-linux, x86-linux, x86_64-linux. Não especifica que é *-linux-gnu, o esperado para extensões nativas do ruby para Linux baseados em GLIBC. Em contraponto, sass-embedded tem extensões nativas para diversos alvos linux, como *-linux-gnu, *-linux-android e *-linux-musl. E advinha? O Alpine Linux é um sistema Linux baseado em MUSL, não em GLIBC. Portanto, meio que por definição, a ABI que o Alpine Linux espera não é compatível com as coisas compiladas para GLIBC.

Vida longa ao rei

Tendo em mente que a ABI das extensões nativas da gem google-protobuf não são compatíveis com Alpine Linux e demais sistemas baseados em MUSL, vamos desistir de tudo e ficar no Debian, não é?

Óbvio que não! Vamos aos fatos!

O mantenedor do sass-embedded colocou o workaround em alguns lugares. Entre eles, no próprio repositório do sass-embedded para ajudar no desenvolvimento da lib tem isso no Gemfile:

# ...

group :development do
  # TODO: https://github.com/protocolbuffers/protobuf/issues/16853
  gem 'google-protobuf', force_ruby_platform: true if RUBY_PLATFORM.include?('linux-musl')
  # ...
end

Ou seja: nessa situação específica de que está rodando em uma plataforma linux-musl, colocar para usar a versão Ruby pura da Gem google-protobuf, sem usar as extensões nativas. Com isso em mente, podemos começar o trabalho de contornar esse problema.

Primeiramente, adicionar a plataformax86_64-linux-musl ao lock:

bundle lock --add-platform x86_64-linux-musl

Então, vamos atualizar as gems com bundle update. O diff importante:

     ffi (1.17.1-arm64-darwin)
     ffi (1.17.1-x86_64-linux-gnu)
+    ffi (1.17.1-x86_64-linux-musl)
 ...
     sass-embedded (1.83.4-x86_64-linux-gnu)
       google-protobuf (~> 4.29)
+    sass-embedded (1.83.4-x86_64-linux-musl)
+      google-protobuf (~> 4.29)
     terminal-table (3.0.2)
 ...
   x86_64-linux
+  x86_64-linux-musl

Muito bem. Mas isso não resolveu a questão do google-protobuf ser forçado ao usar uma plataforma com MUSL. Aplicando cegamente o contorno:

gem 'google-protobuf', force_ruby_platform: true if RUBY_PLATFORM.include?('linux-musl')

Isso não gera nenhunma alteração no lock. Por quê? Porque minha plataforma local é um Mac, não termina em linux-musl, portanto essa linha (que está condicionada a executar apenas quando é linux-musl) não irá ser executada. Então, que tal sempre executar essa linha? Apenas vou pedir para forçar ser Ruby na situação que é um sistema Linux baseado em MUSL?

gem 'google-protobuf', force_ruby_platform: RUBY_PLATFORM.include?('linux-musl')

Pronto, agora pelo menos google-protobuf aparece na lista das dependência. Mas… estou deixando passar algo…

Se a ideia toda dessa atualização é por conta do SASS, para atualizar o SASS, e estou usando o sass-embedded para alcançar isso, por que não atualiaar a gem do SASS também? Porque, se por acaso eu usar o padrão apontado pelo Jekyll na versão que estou segurando

gem "jekyll", "4.3.3" # note que não tem o squigly operator ~>, é exato 4.3.3

posso pegar algum SASS que não está atualizado com as funções que eu estou a usar. Portanto, vamos atualizar no próprio Gemfile também para usar uma versão moderna do sass-embedded:

gem 'sass-embedded', '~> 1.83'

Ok, hora de empurrar e ver se o build funciona bem no Alpine! Caro leitor, saiba que nesse momento estarei salvando o draft e empurrando as diferenças do Gemfile, do lock e deste próprio draft para o blog. Vou rodar o job alpine-test que eu criei no artigo Quebrei o CSS com a publicação anterior, e agora? para ver se deu certo. Mas… na verdade eu testei em um outro branch até funcionar. Estou recriando o que eu fiz lá para transformar em artigo, o que vem não é exatamente surpresa (mas também não fiz os exatos mesmos passos também, apenas aproximadamente e de modo mais controlado).

Fallout

Ok, vamos ver o resultado. Vamos baixar os artefatos. Ele baixa tudo, mas tudo bem, melhor do que baixar cada item criado individualmente pelo Jekyll.

Quando abri o index.html pela primeira vez pensei “pronto, quebrei foi tudo!” O CSS estava todo mal formatado, as coisas ilegíveis, parecia o primeiro instante da abertura do Computaria como descrevi no post Quebrei o CSS com a publicação anterior, e agora?. Então me lembrei que talvez seja só o caminho do CSS que esteja apontando para um lugar que o browser não consegue carregar. Vamos testar essa hipótese?

No index.html, vamos ver como ele aponta para a folha de estilo…

<link rel="stylesheet" href="/blog/css/main.css">

ARRÁ! É isso! Ele tá apontando para um lugar que não existe! No temrinal tratei de criar o link simbólico blog para o diretório atual

ln -s ./ blog

E… continua quebrado? Tá. Quebrei tudo. Abro o main.css em desespero e… ele tá perfeitinho do jeito que eu esperaria ele estar. Então o que houve?

Ah! As referências são todas a /blog, não a ./blog nem a blog. Uma referência assim significa pegar a partir da autoridade (que normalmente é determinado por protocolo/endereço/porta) o caminho. Por exemplo, no caso do Computaria significa https://computaria.gitlab.io. Então, o href em <link rel="stylesheet" href="/blog/css/main.css"> aponta para um recurso em https://computaria.gitlab.io/blog/css/main.css. Mas quem é a autoridade no caso do protocolo file://? Nada por que logo em seguida já começa o path? (Inclusive é por isso que no protocolo file você sempre vê começando com 3 barras, diferente do https://<site>/<caminho>/<do>/<arquivo>.html tem file:///home/<fulano>/<caminho>/<do>/<arquivo>.html).

Então, e se eu mudar para apontar, no lugar de /blog/, apontar para ./blog/? Ou mesmo apenas blog/? Faço o teste e… tudo renderiza normal! Ufa!

Não achei nenhuma deformidade no HTML gerado, nem no CSS gerado que foi o que pegou da última vez. Com isso em mãos, posso voltar a subir as coisas via Alpine.

Vamos retornar o alpine para o estado atual de build.

Revisitando o CI

Eu estava pensando em como aproveitar e deixar o script único. Fui olhar a documentação do GitLab-CI YAML, mais especificamente fui logo em before_script para ver se achava um link para um script global. E eis que acho isso:

Using before_script at the top level […] is deprecated.

Em tradução livre:

Usar before_script diretamente da raiz do arquivo é deprecado.

Hmmmm, e qual a sugestão? Usar default. Ok, vamos usar default então. O que temos de comum? Basicamente:

  • before_script
  • image
  • artifacts

Variáveis tem definição em top-level, então não preciso me preocupar. Ficou assim o default:

default:
  image: ruby:3.2-alpine
  before_script:
    - echo -e "\e[0Ksection_start:`date +%s`:install-deps\r\e[0KInstalando dependêncidas gerais"
    - apk add gcc g++ make
    - gem install bundler
    - echo -e "\e[0Ksection_end:`date +%s`:install-deps\r\e[0K"
    - gem --version
    - echo -e "\e[0Ksection_start:`date +%s`:install-bundle-deps\r\e[0KInstalando dependêncidas via bundle"
    - bundle install
    - echo -e "\e[0Ksection_end:`date +%s`:install-bundle-deps\r\e[0K"
  artifacts:
    when: always
    paths:
    - public
    - Gemfile.lock

Show! Próximo passo? Remover o only:ref e except:ref, são mais duas coisas marcadas para remoção futura. Para isso temos rule. Como funciona? Bem, de modo bem semelhante na real. Mas rule permite eu ter um controle, por exemplo, de fazer um evento de abertura de PR. Legal, né?

Vamos lá. O build manual já está bem definido com o when: manual. Agora, para quando desejamos rodar ao empurrar algo no master? Primeiro, vamos controlar que estamos lidando com o evento de $CI_PIPELINE_SOURCE == "push". Segundo, para quando o alvo for o branch master: $CI_COMMIT_BRANCH == "master". Juntando isso:

pages:
  # ...
  rules:
    - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "master"

De modo semelhante para quando for $CI_COMMIT_BRANCH != "master".

Eu sei que tem um jeito que permitia fazer uma espécie de “herança” de um “job” (ou hidden-job/template) por outro… tem uma documentação toda sobre isso. Veja aqui. Inicialmente eu aprendi a fazer as coisas através de “YAML merge”. Mas segundo a documentação o melhor seria eu fazer com extends. A ideia é a mesma, mas sem magias.

Bem, vamos especificar aqui então um template chamado .run-jekyll. É importante esse . na frente, porque com isso o objeto YAML não é interpretado como um job object.

.run-jekyll:
  script:
    - echo -e "\e[0Ksection_start:`date +%s`:jekyll-build\r\e[0KIniciando o Jekyll"
    - bundle exec jekyll build -d public
    - echo -e "\e[0Ksection_end:`date +%s`:jekyll-build\r\e[0K"

Com isso, o job principal fica assim:

pages:
  extends: .run-jekyll
  rules:
    - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "master"