
Standard Ruby no Apple Silicon e no Docker: um linter, dois runners
TL;DR
O Standard é o RuboCop com um conjunto de regras fixo, debatido pela comunidade, que encerra as brigas sobre estilo. Adicione com standard-rails, fixe ruby_version no .standard.yml e adote com --fix e --generate-todo. Um pequeno script bin/standardrb roda nativo no Apple Silicon e no Docker nas demais máquinas; com um único lockfile, os dois dão o mesmo resultado.
O Standard dá aos times Ruby um linter e formatador sem nada para discutir. Veja o que ele faz, como ele se apoia no RuboCop e um pequeno script que o executa nativamente no Apple Silicon e no Docker em todo o resto, com resultados idênticos.
Todo time Ruby já teve essa discussão. Aspas simples ou duplas. Vírgulas finais. Alinhamento de hashes. Alguém ajusta o .rubocop.yml do seu jeito, outra pessoa reverte uma regra no PR seguinte, e o linter vira fonte de churn em vez de rede de segurança.
O Standard encerra essa conversa ao se recusar a tê-la. Este post cobre o que a gem oferece, como ela funciona por dentro e um setup prático para times cujo app Rails roda no Docker Compose: lint nativo em Macs com Apple Silicon, fallback para o container em todo o resto e a mesma resposta nos dois casos.
O que é o Standard
O Standard (gem standard, executável standardrb) é um linter e formatador Ruby mantido pela Test Double desde 2018. Ele traz a filosofia do StandardJS para o Ruby: um único conjunto de regras, e você não pode configurá-lo.
Por baixo dos panos, é o RuboCop mais o rubocop-performance, com o estado (ativo ou não) e as configurações de cada cop já decididos para você. Esse conjunto de regras é fruto de anos de debate na comunidade, e novas versões saem mais ou menos todo mês para acompanhar os novos cops do RuboCop.
O argumento a favor não é que as escolhas do Standard sejam as melhores possíveis. É que o valor de um linter vem de usá-lo de forma consistente, não de como ele está ajustado, e um conjunto fixo de regras elimina uma categoria inteira de comentários de review e de churn de configuração.
Recursos que valem a pena conhecer
- Lint e formatação em uma só ferramenta.
standardrbreporta as ofensas.standardrb --fixaplica as autocorreções consideradas seguras.standardrb --fix-unsafelyaplica também as que podem mudar o comportamento, então rode com a working tree limpa e deixe seus testes julgarem. - Tarefas Rake. Adicione
require "standard/rake"ao seu Rakefile para terrake standard,rake standard:fixerake standard:fix_unsafely. Flags de CLI passam pela variável de ambienteSTANDARDOPTS. - Válvulas de escape sem guerra de configuração. Desative um cop em uma linha ou bloco com
# standard:disable Cop/Name(e# standard:enable), ignore arquivos inteiros ou globs, ou ignore cops específicos para um glob no.standard.yml. - Adoção incremental.
standardrb --generate-todogera um.standard_todo.ymlcom as ofensas existentes por arquivo. Código novo segue o conjunto completo de regras enquanto você reduz o backlog. - Plugins.
standard-railsestandard-sorbetadicionam cops específicos de framework, e você pode escrever os seus com a gemlint_roller. Plugins adicionam regras; não podem afrouxar as do Standard. extend_config. Aponte o Standard para arquivos YAML comuns do RuboCop para incluir cops extras (de uma gem como a betterlint, por exemplo) sem empacotar um plugin.- Suporte a editores via LSP. O Standard inclui um add-on para o Ruby LSP que carrega automaticamente quando a gem está no seu Gemfile, com pull diagnostics, formatação e code actions de quick fix. Para editores que não usam o Ruby LSP,
standardrb --lspinicia um language server independente. - Qualquer formatter do RuboCop.
--format progress,--format jsonou--format githubpara anotações inline no GitHub Actions.
Como funciona
O Standard não faz fork do RuboCop. Cada execução segue mais ou menos este roteiro:
- Lê as opções. Flags de CLI mais o
.standard.yml, que o Standard encontra subindo a partir do diretório atual. - Monta uma configuração do RuboCop em memória. O conjunto base de regras do Standard entra primeiro, depois seus plugins padrão (
standard-customestandard-performance), depois os plugins que você listar e, por fim, quaisquer arquivos deextend_config. A configuração é first-in-wins: depois que uma camada configura um cop, as camadas seguintes não podem alterá-lo. É assim que o Standard continua "inconfigurável" e, ainda assim, extensível. - Resolve o Ruby alvo.
ruby_versionvira oTargetRubyVersiondo RuboCop. Se você não definir, ele é inferido de arquivos como.ruby-version,.tool-versions,Gemfile.lockou um gemspec. - Aplica os ignores. Os ignores padrão, seus globs de
ignore:e o arquivo de todo. - Passa o controle para o RuboCop. Cops, autocorreção, cache de resultados e formatters são todos do RuboCop.
Três consequências práticas:
- Atualizar o Standard atualiza o RuboCop e o conjunto de regras juntos. Seu
Gemfile.lockfixa os dois, então você faz o bump no seu ritmo. - Com o mesmo lockfile, o mesmo
.standard.ymle o mesmo Ruby alvo, os resultados são determinísticos, não importa onde o Standard rode. É essa propriedade que torna segura a divisão entre host e Docker mais abaixo. - Qualquer ferramenta que entende a saída do RuboCop já entende a do Standard.
Configurando em um app Rails
# Gemfile
group :development, :test do
gem "standard", require: false
gem "standard-rails", require: false
end
# .standard.yml
ruby_version: 3.4 # match .ruby-version; pinning removes any host vs. container ambiguity
parallel: true
format: progress
plugins:
- standard-rails # Rails version is usually detected; pin with target_rails_version if needed
ignore:
- "db/schema.rb"
- "db/*_schema.rb"
- "bin/**/*"
- "vendor/**/*"
- "node_modules/**/*"
- "tmp/**/*"
# Rakefile
require "standard/rake"
Se o app veio de um rails new no Rails 7.2 ou posterior, ele já vem com rubocop-rails-omakase. Para trocar, remova essa gem, o .rubocop.yml e o bin/rubocop, e atualize a etapa de lint no CI. Adote em dois passos:
bundle exec standardrb --fix # take every safe autocorrect
bundle exec standardrb --generate-todo # park whatever is left
Faça o commit disso separado do trabalho de features para que o diff continue revisável.
Nativo no Apple Silicon, Docker em todo o resto
Esta é a situação que esta seção resolve. O app roda no Docker Compose, e rodar o lint via docker compose run funciona em qualquer máquina. Mas cada invocação sobe um container e lê o projeto pelo compartilhamento de arquivos do Docker Desktop. Isso não é problema no CI e é tolerável de vez em quando, mas fica lento em um hook de pre-commit.
Em Macs com Apple Silicon, um Ruby nativo é rápido de instalar com mise, rbenv ou asdf. Gems nativas comuns como a pg publicam builds pré-compilados para arm64-darwin. Ali, rodar o Standard no host é rápido e barato de manter.
Em Macs Intel, estações Linux ou qualquer máquina sem um Ruby correspondente no host, o container continua sendo o caminho confiável.
Como os dois caminhos leem o mesmo Gemfile.lock e o mesmo .standard.yml, eles produzem os mesmos resultados. A única decisão é qual runner usar, e um script pode tomar essa decisão.
Preparando o host
# Install the Ruby the image uses
mise install ruby@"$(cat .ruby-version)" # or rbenv/asdf
# Make sure container-oriented Bundler settings are not leaking onto the host
env | grep BUNDLE_ # unset BUNDLE_PATH, BUNDLE_APP_CONFIG, BUNDLE_DEPLOYMENT, BUNDLE_WITHOUT
cat .bundle/config 2>/dev/null # remove container paths such as /usr/local/bundle
bundle install
bundle exec standardrb
Quatro pegadinhas aparecem quando o lockfile só foi resolvido dentro do Linux:
- Plataformas do lockfile. A primeira instalação no host adiciona
arm64-darwinaoGemfile.lock, e o modo frozen ou deployment se recusa a fazer isso. Adicione a plataforma uma vez e faça commit:bundle lock --add-platform arm64-darwin. vendor/bundlecompartilhado. Se o container instala gems emvendor/bundledentro do bind mount, não instale no mesmo caminho no host. As extensões nativas de Linux e de Darwin vão se sobrescrever. Deixe o host usar o gem home padrão do seu gerenciador de versões.- Gems nativas. Desde a 1.6, a
pgpublica gems pré-compiladas por plataforma com a libpq embutida, então o host geralmente não precisa do PostgreSQL do Homebrew. Gems sem builds binários, como amysql2, ainda precisam das bibliotecas cliente instaladas localmente, mesmo que o lint nunca as carregue. - Terminais com Rosetta. Se o seu app de terminal está com a opção "Abrir com o Rosetta" ativada,
uname -mreportax86_64em um Mac com Apple Silicon. O script abaixo leva isso em conta, mas um terminal nativo continua sendo o melhor padrão.
O script runner
Salve isto como bin/standardrb e rode chmod +x nele. (Se algum dia você rodar bundle binstubs standard, não deixe que ele sobrescreva este arquivo.)
#!/bin/sh
# Run Standard natively on Apple Silicon, in Docker everywhere else.
# Override with STANDARDRB_RUNNER=local|docker.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
is_apple_silicon() {
[ "$(uname -s)" = "Darwin" ] || return 1
# Native arm64 shell
[ "$(uname -m)" = "arm64" ] && return 0
# x86_64 shell translated by Rosetta, which only exists on Apple Silicon
[ "$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)" = "1" ] && return 0
# Hardware flag as a last check
[ "$(sysctl -n hw.optional.arm64 2>/dev/null || echo 0)" = "1" ]
}
local_ready() {
command -v bundle >/dev/null 2>&1 && bundle check >/dev/null 2>&1
}
run_local() {
exec bundle exec standardrb "$@"
}
run_docker() {
exec docker compose run --rm -T app bundle exec standardrb "$@"
}
case "${STANDARDRB_RUNNER:-auto}" in
local) run_local "$@" ;;
docker) run_docker "$@" ;;
auto) ;;
*) echo "STANDARDRB_RUNNER must be auto, local or docker" >&2; exit 64 ;;
esac
if is_apple_silicon; then
if local_ready; then
run_local "$@"
fi
echo "standardrb: host bundle not ready (run bundle install); using Docker" >&2
fi
run_docker "$@"
Uso:
bin/standardrb # lint
bin/standardrb --fix # autocorrect
bin/standardrb app/models/user.rb # specific files
STANDARDRB_RUNNER=docker bin/standardrb # force the container, e.g. to compare runners
Como ele decide:
- Detecção de Apple Silicon. Ele verifica três coisas, nesta ordem: um shell
arm64nativo, um shell traduzido pelo Rosetta (sysctl.proc_translatedigual a1) e, por fim, a flag de hardwarehw.optional.arm64. Verificar só ouname -merra em terminais com Rosetta, e a flag de hardware sozinha pode não estar visível a partir de um processo traduzido. - Fallback quando o host não está pronto.
bundle checkconfirma que todas as gems do lockfile estão instaladas antes de o script se comprometer com o host. Se falhar, o script imprime uma dica de uma linha e usa o Docker em vez de dar erro. - Exit codes repassados.
execsubstitui o shell, então hooks e CI falham quando há ofensas, com qualquer um dos runners. - TTY do container.
-Tdesativa a alocação de TTY para que o caminho do container funcione dentro de hooks e de outros shells não interativos. (--no-TTYé a mesma flag; você só precisa de uma.)
Duas ressalvas:
PATHem hooks e clientes GUI. Hooks do Git e clientes Git com interface gráfica costumam rodar sem os shims do seu gerenciador de versões noPATH. Nesse caso, o script cai para o Docker. Se quiser o caminho nativo ali também, adicioneeval "$(mise activate sh)"(ou o equivalente do rbenv) perto do topo.- Dono dos arquivos no Linux. Em um host Linux,
--fixvia Docker grava os arquivos como o usuário do container, muitas vezes root. Adicione--user "$(id -u):$(id -g)"à linha dodocker compose runse o caminho de gems da imagem puder ser lido por esse usuário.
Integrando ao seu fluxo de trabalho
Editor. Use o Ruby LSP com o add-on do Standard. O language server fica rodando o tempo todo e conversa diretamente com o bundle do host; o script não participa.
// .vscode/settings.json
{
"rubyLsp.rubyVersionManager": { "identifier": "mise" },
"rubyLsp.formatter": "standard",
"rubyLsp.linters": ["standard"],
"[ruby]": {
"editor.defaultFormatter": "Shopify.ruby-lsp",
"editor.formatOnSave": true
}
}
Desative qualquer extensão separada de Standard ou de RuboCop para evitar diagnósticos duplicados. Em máquinas sem Ruby no host, abra o projeto em um Dev Container para que o Ruby LSP rode dentro dele.
Pre-commit. Encaminhe o hook pelo script para que cada máquina escolha o runner certo:
# lefthook.yml
pre-commit:
commands:
standard:
glob: "*.{rb,rake}"
run: bin/standardrb --fix --force-exclusion {staged_files}
stage_fixed: true
O --force-exclusion faz diferença aqui. Quando os arquivos são passados explicitamente, o RuboCop os analisa mesmo que correspondam a um glob de ignore:, então, sem a flag, um db/schema.rb em stage é analisado, e possivelmente reescrito, depois de cada migration.
CI. Pule a detecção. Chame o Standard diretamente no ambiente que o CI já usa, seja a imagem do app ou o ruby/setup-ruby, e use o formatter do GitHub para anotações inline:
bundle exec standardrb --format github
Conclusão
O Standard elimina o debate de estilo por design e, como é RuboCop por baixo, se encaixa em qualquer ferramenta que já fala RuboCop. Fixe o Ruby alvo no .standard.yml, mantenha um único lockfile, e a questão de onde ele roda vira um detalhe de performance, não de corretude: nativo no Apple Silicon pela velocidade, Docker em todo o resto pela consistência, e o mesmo veredito nos dois.
Fontes
- Standard Ruby (README) github.com
- Wiki do Standard Ruby: VS Code com Ruby LSP github.com
- Plugin standard-rails github.com
- Configuração do RuboCop: definindo a versão alvo do Ruby docs.rubocop.org
- Ruby LSP github.com
- Gem pg no RubyGems.org rubygems.org
- Referência do docker compose run docs.docker.com
- Lefthook github.com
Perguntas frequentes
Posso mudar as regras do Standard?
Não. Você pode desativar um cop em uma linha ou bloco com `# standard:disable`, ignorar arquivos ou cops específicos por glob no `.standard.yml` e adicionar regras via plugins ou `extend_config`. O que não dá é reconfigurar um cop que o Standard já configura, porque a configuração dele é first-in-wins.
Qual a diferença entre o Standard e o rubocop-rails-omakase?
Os dois são configurações do RuboCop. O omakase é o padrão em apps Rails novos desde a 7.2 e fica em um `.rubocop.yml` editável. O Standard mantém o conjunto de regras fixo e traz CLI, tarefas Rake, arquivo de todo e add-on para o Ruby LSP próprios. Escolha um: as regras de layout dos dois divergem, então rodar ambos gera autocorreções conflitantes.
A execução no host e no Docker dão mesmo o mesmo resultado?
Sim, desde que as duas leiam o mesmo `Gemfile.lock` (mesmas versões de Standard e RuboCop), o mesmo `.standard.yml` e o mesmo Ruby alvo. Fixar `ruby_version` no `.standard.yml` elimina a última variável entre os dois runners.
Preciso do PostgreSQL instalado no Mac para rodar o lint no host?
Geralmente não. Desde a 1.6, a `pg` publica gems pré-compiladas para `arm64-darwin` com a libpq incluída. Gems sem builds binários, como a `mysql2`, ainda precisam das bibliotecas cliente para instalar, mesmo que o Standard nunca as carregue.
Como adotar o Standard em uma base de código grande que já existe?
Rode `standardrb --fix` para aplicar todas as autocorreções seguras e depois `standardrb --generate-todo` para registrar as ofensas restantes no `.standard_todo.yml`. Código novo segue o conjunto completo de regras desde o primeiro dia, e você apaga as entradas do todo à medida que corrige. Faça commit dos dois passos separado do trabalho de features.
Por que o script cai para o Docker no meu Mac com Apple Silicon?
Ou o `bundle` não está no `PATH`, ou o `bundle check` falhou. Hooks do Git e clientes GUI costumam pular os shims do gerenciador de versões, então ative o mise ou o rbenv no topo do script, rode `bundle install` no host ou force o caminho nativo com `STANDARDRB_RUNNER=local`.
Comentários
Faça login com GitHub para comentar.
Ainda não há comentários.