Do CRAN ao R-universe: o teto de 10 MB, a linha de política, e o que continua humano

R
CRAN
runiverse
packaging
Rust
agents
opensource
Um relato honesto de uma submissão ao CRAN que não virou release. O que a automação cran_publisher tratou, o que ela aprendeu com os pretests, e onde ela parou porque automação alguma atravessa uma linha de política. Com a aritmética de tamanho, o pedido do revisor, e a mudança para o R-universe.
Autor

Pedro Carvalho Brom

Data de Publicação

4 de junho de 2026

Code repositorypcbrom/gpumetropolis

Em maio de 2026 submeti gpumetropolis 0.1.0, um pacote R que entrega um amostrador Metropolis-Hastings com kernel de log-densidade portável para GPU, ao CRAN. O pacote passou no segundo pretest. Um revisor humano então me pediu para fazer algo que o pacote não conseguia fazer sem perder o que era. Retirei a submissão e movi a distribuição para o R-universe.

Este post é o arco, escrito como aconteceu: as partes que a automação cran_publisher tratou, as partes que ela aprendeu com a revisão real, e a decisão que uma automação jamais deveria tomar sozinha. Não é uma reclamação contra o CRAN; é um pequeno estudo de caso sobre a linha entre o que ferramentas executam e o que pessoas decidem.

O pacote, em um parágrafo

O gpumetropolis permite ao usuário declarar um modelo escrevendo a log-verossimilhança e a log-priori como fórmulas R comuns. O pacote compila para um bytecode de máquina de pilha que um único kernel CubeCL interpreta, então o mesmo kernel roda em backends de CPU e GPU (CUDA, ROCm, Vulkan, CPU). O amostrador avança muitas cadeias independentes em um passo batched. O motivo de os backends de GPU importarem para este post é mecânico: vendorizar suas árvores de dependências Rust empurra o tarball para acima da política de tamanho do CRAN. O restante do que o pacote faz pertence a outro post.

Ato 1: a submissão e o que a automação aprendeu

A submissão foi o primeiro uso em produção real do cran_publisher, uma skill que roda o R CMD check local, classifica os problemas, aplica correções por um loop, e emite um relatório de submissão. O pipeline é o tipo de coisa que um mantenedor monta à mão a partir de scripts espalhados; essa versão é uma ferramenta única, auditada, e roda como pré-voo antes de cada upload.

O primeiro upload foi arquivado no pretest de incoming feasibility do CRAN. O pretest sinalizou uso de múltiplos núcleos além de dois threads no backend Rust (um pool Rayon que o build cargo padrão usava). A política do CRAN sobre paralelismo em checks é explícita: não exceder dois núcleos. O fix foi capar o pool Rayon a dois threads sob R CMD check. Depois do fix e de uma resubmissão, o segundo pretest devolveu 1 NOTE no Debian e 1 NOTE no Windows. A NOTE única incluía o marcador de nova submissão, dois falsos positivos de ortografia (Gelman e bytecode), e o tamanho do tarball: 44851357 bytes.

A automação aprendeu com o primeiro arquivamento. Daqui em diante, submission_preflight trata qualquer uso de múltiplos núcleos além de dois threads sob R CMD check como gate bloqueante. Esse é o tipo certo de aprendizado que se espera de uma camada de tooling: as regras da plataforma são codificadas em checks, então pacotes futuros submetidos com a mesma skill nunca repetem a mesma chegada ao pretest. A skill não pode antecipar toda política do CRAN; pode codificar cada uma à medida que é observada.

Ato 2: o teto de 10 MB

A revisão humana então começou. O revisor do CRAN, Benjamin Altmann, mandou dois pedidos. O primeiro foi de tamanho: “A CRAN package should not be larger than 10 MB. Please reduce the size.” O segundo foi uma escrita em filespace de usuário sinalizada em dois scripts auxiliares (tools/msrv.R e tools/config.R), que é direta de corrigir e não foi o que parou a submissão.

O tamanho era a linha de política. O tarball estava em 44,85 MB, dos quais 43 MB eram os crates Rust vendorizados. A razão de os crates terem que ser vendorizados é o próprio ambiente de build do CRAN: ele constrói pacotes offline, então qualquer crate externo que o build precise tem que viajar junto com o tarball-fonte. A árvore de vendoring do gpumetropolis é dominada pelas stacks CUDA e Vulkan do CubeCL, que são os backends que tornam o pacote útil.

Vendorizar parcialmente seria viável, descartando as stacks de GPU para o tarball do CRAN e reconstruindo a partir de um Cargo.lock só de CPU? Um teste rápido de cargo build --offline respondeu não. Vendoring parcial não é como o cargo resolve builds offline: o lockfile exige a árvore completa de dependências que o manifest declara, incluindo crates opcionais. Para descer abaixo de 10 MB, o pacote teria que remover os backends de GPU do manifest por completo, abrindo mão da propriedade que o torna útil.

Essa aritmética foi onde a automação parou. A skill cran_publisher consegue ler uma política de tamanho, contar bytes, propor estratégias; não consegue decidir se o pacote deve manter sua identidade ou perder uma feature para caber num teto de tarball. Essa decisão é um juízo sobre para que o pacote serve. Automação alguma atravessa uma linha de política dessa forma, e não deveria tentar.

Ato 3: a mudança e a extensão

Retirei a submissão em 22 de maio de 2026 e publiquei o pacote no R-universe. O R-universe é o canal de distribuição para R do Jeroen Ooms, construído em cima do GitHub Actions; não impõe um teto de 10 MB ao tarball. O pacote inteiro vai, backends de GPU incluídos. Os runners do R-universe não têm os toolkits de GPU instalados, então os binários pré-construídos no canal são CPU-only por construção; usuários que querem um binário com GPU instalam a partir do fonte em um host com o toolkit, e o pacote detecta automaticamente.

A skill cran_publisher foi então estendida com um módulo novo, runiverse.py, que adiciona runiverse_preflight, runiverse_register e runiverse_status. A superfície total agora é de nove funções, seis para o canal CRAN e três para R-universe. A suíte de testes manteve-se verde em 318 expectations durante a extensão. A skill é uma ferramenta, dois canais.

O trade-off, escrito sem floreio

O R-universe não substitui o CRAN. O CRAN revisa pacotes por uma equipe pequena e paga, e oferece um sinal de qualidade que um canal de integração contínua não consegue. O R-universe é um canal de integração contínua: builds rodam a cada commit, pacotes são descobríveis, mas não há revisor humano fazendo a chamada de que um mantenedor não prometeu demais. Para alguns pacotes, o sinal CRAN vale custos de refactor significativos; para o gpumetropolis, o custo de refactor era o próprio pacote.

Não generalizaria a partir deste caso único. O ponto é o formato da decisão. Uma vez que a linha de política de um host aparece, ferramenta alguma decide se o pacote deve ser redesenhado para caber nela. A forma correta de escrever a automação é deixá-la rodar até onde dá, marcar a linha claramente e parar.

Onde isso deixa o pacote e a skill

gpumetropolis 0.1.0 está no R-universe com binários construídos para treze plataformas; a skill cran_publisher cobre dois canais de distribuição com o mesmo contrato audit-first; a próxima submissão CRAN com a skill não vai repetir o erro de múltiplos núcleos. Nenhum desses três itens responde “o gpumetropolis vai um dia para o CRAN?” Não sei. Se os backends de GPU um dia estabilizarem em um layout que permita à árvore vendorizada cair a uma fração do tamanho atual, a pergunta reabre. Até lá, R-universe é o lugar certo.

Se você mantém pacotes R e usa uma camada de tooling assim, a pergunta aberta para mim é a mesma da versão LinkedIn deste post: onde vocês traçam a linha entre o que automatizam e o que decidem à mão? Tenho curiosidade genuína de como outros marcam.

Instalação

install.packages("gpumetropolis",
                 repos = c("https://pcbrom.r-universe.dev",
                           "https://cloud.r-project.org"))

Referências