Formato de distribuição binária

O formato de distribuição binária (wheel) foi originalmente definido na PEP 427. A versão atual da especificação está aqui.

Abstrato

Esta PEP descreve um formato de pacote construído para Python chamado “wheel”.

Um wheel é um arquivo em formato ZIP com um nome de arquivo formatado de forma especial e a extensão .whl. Ele contém uma única distribuição quase como seria instalado de acordo com a PEP 376 com um esquema de instalação particular. Embora um instalador especializado seja recomendado, um arquivo wheel pode ser instalado simplesmente descompactando em pacotes de sites com a ferramenta padrão de “descompactação” enquanto preserva informações suficientes para espalhar seu conteúdo em seus caminhos finais a qualquer momento.

Aceitação da PEP

Esta PEP foi aceita, e a versão do wheel definida foi atualizada para 1.0, por Nick Coghlan em 16 de fevereiro de 2013 1

Justificativa

Python precisa de um formato de pacote mais fácil de instalar do que sdist. Os pacotes sdist do Python são definidos e requerem os sistemas de construção distutils e setuptools, executando código arbitrário para construir e instalar e recompilar o código apenas para que possa ser instalado em um novo virtualenv. Este sistema de combinação entre construção e instalação é lento, difícil de manter e impede a inovação tanto nos sistemas de construção quanto nos instaladores.

O wheel tenta remediar esses problemas fornecendo uma interface mais simples entre o sistema de compilação e o instalador. O formato do pacote binário wheel libera os instaladores de ter que saber sobre o sistema de compilação, economiza tempo amortizando o tempo de compilação em muitas instalações e elimina a necessidade de instalar um sistema de compilação no ambiente de destino.

Detalhes

Instalando um wheel ‘distribution-1.0-py32-none-any.whl’

A instalação do wheel consiste, em teoria, em duas fases:

  • Desempacotar.

    1. Analisar distribution-1.0.dist-info/WHEEL.

    2. Verificar se o instalador é compatível com a versão Wheel. Avisa se a versão secundária é maior, cancela se a versão principal é maior.

    3. Se Root-Is-Purelib == ‘true’, descompactar o arquivo em purelib (sites-packages).

    4. Caso contrário, descompactar o arquivo em platlib (site-packages).

  • Espalhar.

    1. O arquivo descompactado inclui distribution-1.0.dist-info/ e (se houver dados) distribution-1.0.data/.

    2. Mover cada subárvore de distribution-1.0.data/ em seu caminho de destino. Cada subdiretório de distribution-1.0.data/ é uma chave para um dicionário de diretórios de destino, como distribution-1.0.data/(purelib|platlib|headers|scripts|data). Os caminhos inicialmente suportados são obtidos de distutils.command.install.

    3. Se aplicável, atualizar os scripts começando com #!python para apontar para o interpretador correto.

    4. Atualizar distribution-1.0.dist-info/RECORD com os caminhos instalados.

    5. Remover o diretório vazio distribution-1.0.data.

    6. Compilar qualquer .py instalado em .pyc. (Os desinstaladores devem ser inteligentes o suficiente para remover .pyc, mesmo que não seja mencionado em RECORD.)

Formato de arquivos

Convenção de nome de arquivos

O nome de arquivo do wheel é {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl.

distribution

Nome da distribuição (p.ex., ‘django’, ‘pyramid’).

version

Versão da distribuição (p.ex., 1.0).

build tag

Número de construção opcional. Deve começar com um dígito. Atua como um desempate se os nomes de arquivo de dois wheels forem iguais em todos os outros aspectos (ou seja, nome, versão e outras tags). Classifique como uma tupla vazia se não for especificado, caso contrário, classifique como uma tupla de dois itens com o primeiro item sendo os dígitos iniciais como um int, e o segundo item sendo o restante da tag como um str.

implementação da linguagem e tag de versão

p.ex., ‘py27’, ‘py2’, ‘py3’.

abi tag

p.ex., ‘cp33m’, ‘abi3’, ‘none’.

platform tag

p.ex., ‘linux_x86_64’, ‘any’.

Por exemplo, distribution-1.0-1-py27-none-any.whl é a primeira construção de um pacote chamado ‘distribution’ e é compatível com Python 2.7 (qualquer implementação Python 2.7), com sem ABI (puro Python), em qualquer arquitetura de CPU.

Os últimos três componentes do nome do arquivo antes da extensão são chamados de “tags de compatibilidade”. As tags de compatibilidade expressam os requisitos básicos do interpretador do pacote e são detalhadas na PEP 425.

Escape e Unicode

Como os componentes do nome do arquivo são separados por um traço (-, HYPHEN-MINUS), este caractere não pode aparecer em nenhum componente. Isso é tratado da seguinte maneira:

  • Em nomes de distribuição, qualquer ocorrência de caracteres -_. (HYPHEN-MINUS, LOW LINE e FULL STOP) deve ser substituída por _ (LOW LINE) e caracteres maiúsculos devem ser substituídos pelos minúsculos correspondentes. Isso é equivalente à normalização de nome regular da PEP 503 seguida pela substituição de - por _. Porém, ferramentas que usem wheels devem estar preparadas para aceitar . (FULL STOP) e letras maiúsculas, pois estes eram permitidos por uma versão anterior desta especificação.

  • Os números de versão devem ser normalizados de acordo com a PEP 440. Os números de versão normalizados não podem conter -.

  • Os componentes restantes não podem conter caracteres -, portanto, nenhum escape é necessário.

Ferramentas que produzem wheels devem verificar se os componentes do nome de arquivo não contêm -, pois o arquivo resultante pode não ser processado corretamente se contiverem.

O nome do arquivo é Unicode. Levará algum tempo até que as ferramentas sejam atualizadas para oferecer suporte a nomes de arquivos não ASCII, mas eles são suportados nesta especificação.

Os nomes de arquivos dentro do arquivo são codificados como UTF-8. Embora alguns clientes ZIP em uso comum não exibam nomes de arquivo UTF-8 apropriadamente, a codificação é suportada pela especificação ZIP e pelo zipfile do Python.

Conteúdo dos arquivos

O conteúdo de um arquivo wheel, onde {distribution} é substituído pelo nome do pacote, por exemplo, beaglevote e {version} é substituído por sua versão, p.ex., 1.0.0, consiste em:

  1. /, a raiz do arquivo, contém todos os arquivos a serem instalados em purelib ou platlib conforme especificado em WHEEL. purelib e platlib são normalmente site-packages.

  2. {distribution}-{version}.dist-info/ contém metadados.

  3. {distribution}-{version}.data/ contém um subdiretório para cada chave de esquema de instalação não vazia ainda não coberta, onde o nome do subdiretório é um índice em um dicionário de caminhos de instalação (p.ex., data, scripts, headers, purelib, platlib).

  4. Scripts Python devem aparecer em scripts e começar exatamente com b'#!python' para desfrutar da geração do wrapper de script e reescrever #!python no momento da instalação. Eles podem ter qualquer ou nenhuma extensão.

  5. {distribution}-{version}.dist-info/METADATA é Metadata versão 1.1 ou metadados de formato superior.

  6. {distribution}-{version}.dist-info/WHEEL são metadados sobre p arquivo em si no mesmo formato “chave: valor”:

    Wheel-Version: 1.0
    Generator: bdist_wheel 1.0
    Root-Is-Purelib: true
    Tag: py2-none-any
    Tag: py3-none-any
    Build: 1
    
  7. Wheel-Version é o número de versão da especificação Wheel.

  8. Generator é o nome e, opcionalmente, a versão do software que produziu o arquivo.

  9. Root-Is-Purelib é verdadeiro se o diretório de nível superior do arquivo deve ser instalado em purelib; caso contrário, a raiz deve ser instalada no platlib.

  10. Tag são as tags de compatibilidade expandida do wheel; no exemplo, o nome do arquivo conteria py2.py3-none-any.

  11. Build é o número da construção e é omitido se não houver número da construção.

  12. Um instalador de wheel deve avisar se Wheel-Version é maior do que a versão que ele suporta, e deve falhar se Wheel-Version tiver uma versão principal maior do que a versão que ele suporta.

  13. O Wheel, sendo um formato de instalação destinado a funcionar em várias versões do Python, geralmente não inclui arquivos .pyc.

  14. Wheel não contém setup.py ou setup.cfg.

Esta versão da especificação do wheel é baseada nos esquemas de instalação do distutils e não define como instalar arquivos em outros locais. O layout oferece um superconjunto da funcionalidade fornecida pelos formatos binários wininst e egg existentes.

O diretório .dist-info
  1. Os diretórios .dist-info de wheels incluem no mínimo METADATA, WHEEL e RECORD.

  2. METADATA são os metadados do pacote, o mesmo formato do PKG-INFO encontrado na raiz dos sdists.

  3. WHEEL são os metadados de wheel específicos para uma construção do pacote.

  4. RECORD é uma lista de (quase) todos os arquivos no wheel seus hashes seguros. Ao contrário da PEP 376, nenhum arquivo, exceto RECORD, pode conter um hash de si mesmo, deve incluir seu hash. O algoritmo hash deve ser sha256 ou melhor; especificamente, md5 e sha1 não são permitidos, pois os arquivos wheel assinados contam com hashes fortes em RECORD para validar a integridade do arquivo.

  5. INSTALLER e REQUESTED da PEP 376 não são incluídos no arquivo.

  6. RECORD.jws é usado para assinaturas digitais. Não é mencionado no RECORD.

  7. RECORD.p7s é permitido como cortesia para qualquer pessoa que prefira usar assinaturas S/MIME para proteger seus arquivos wheels. Não é mencionado no RECORD.

  8. Durante a extração, os instaladores de wheels verificam todos os hashes em RECORD em relação ao conteúdo do arquivo. Além de RECORD e suas assinaturas, a instalação falhará se qualquer arquivo no arquivo não for mencionado e com hash correto em RECORD.

O diretório .data

Qualquer arquivo que não é normalmente instalado dentro de site-packages vai para o diretório .data, nomeado como o diretório .dist-info, mas com a extensão .data/:

distribution-1.0.dist-info/

distribution-1.0.data/

O diretório .data contém subdiretórios com os scripts, cabeçalhos, documentação e assim por diante da distribuição. Durante a instalação, o conteúdo desses subdiretórios é movido para seus caminhos de destino.

Arquivos wheels assinados

Os arquivos wheels incluem um RECORD estendido que permite assinaturas digitais. O RECORD da PEP 376 foi alterado para incluir um hash seguro digestname=urlsafe_b64encode_nopad(digest) (codificação base64 de urlsafe sem caracteres ao final =) como a segunda coluna em vez de um md5sum. Todas as entradas possíveis são hash, incluindo quaisquer arquivos gerados, como arquivos .pyc, mas não RECORD, que não pode conter seu próprio hash. Por exemplo:

file.py,sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\_pNh2yI,3144
distribution-1.0.dist-info/RECORD,,

O(s) arquivo(s) de assinatura RECORD.jws e RECORD.p7s não são mencionados em RECORD, pois eles só podem ser adicionados após RECORD ser gerado. Todos os outros arquivos no arquivo devem ter um hash correto em RECORD ou a instalação falhará.

Se assinaturas da web JSON forem usadas, uma ou mais assinaturas JSON Web Signature JSON Serialization (JWS-JS) serão armazenadas em um arquivo RECORD.jws adjacente a RECORD. JWS é usado para assinar RECORD incluindo o hash SHA-256 de RECORD como a carga JSON da assinatura:

{ "hash": "sha256=ADD-r2urObZHcxBW3Cr-vDCu5RJwT4CaRTHiFmbcIYY" }

(O valor hash é o mesmo formato usado em RECORD.)

Se RECORD.p7s for usado, ele deve conter uma assinatura de formato S/MIME separada de RECORD.

Um instalador de wheel não precisa entender as assinaturas digitais, mas DEVE verificar os hashes em RECORD em relação ao conteúdo do arquivo extraído. Quando o instalador verifica os hashes do arquivo em relação ao RECORD, um verificador de assinatura separado só precisa estabelecer se RECORD corresponde à assinatura.

Veja

Comparação com .egg

  1. Wheel é um formato de instalação; ovo é importável. Os arquivos wheel não precisam incluir .pyc e estão menos vinculados a uma versão ou implementação específica do Python. O Wheel pode instalar pacotes (puro Python) construídos com versões anteriores do Python, portanto, você nem sempre precisa esperar que o empacotador os atualize.

  2. O Wheel usa diretórios .dist-info; egg usa .egg-info. Wheel é compatível com o novo mundo da empacotamento de Python e os novos conceitos que ele traz.

  3. O Wheel tem uma convenção de nomenclatura de arquivo mais rica para o mundo de múltiplas implementações de hoje. Um único arquivo de wheel pode indicar sua compatibilidade com várias versões e implementações de linguagem Python, ABIs e arquiteturas de sistema. Historicamente, a ABI tem sido específica para uma versão do CPython, o wheel estando pronto para a ABI estável.

  4. O wheel não causa perdas. A implementação do primeiro wheel, bdist_wheel, sempre gera egg-info e, então, as converte em .whl. Também é possível converter eggs existentes e distribuições de bdist_wininst.

  5. Wheel é versionado. Cada arquivo wheel contém a versão da especificação wheel e a implementação que a empacotou. Esperançosamente, a próxima migração pode ser simplesmente para Wheel 2.0.

  6. Wheel é uma referência a outro Python.

FAQ

Wheel define um diretório .data. Devo colocar todos os meus dados lá?

Esta especificação não tem uma opinião sobre como você deve organizar seu código. O diretório .data é apenas um lugar para quaisquer arquivos que não são normalmente instalados dentro de site-packages ou no PYTHONPATH. Em outras palavras, você pode continuar a usar pkgutil.get_data(package, resource) ainda que esses arquivos normalmente não sejam distribuídos no diretório .data do wheel.

Por que o wheel inclui assinaturas anexadas?

Assinaturas anexadas são mais convenientes do que assinaturas separadas porque elas viajam com o arquivo. Uma vez que apenas os arquivos individuais são assinados, o arquivo pode ser recompactado sem invalidar a assinatura ou os arquivos individuais podem ser verificados sem ter que baixar todo o arquivo.

Por que wheel permite assinaturas JWS?

As especificações JOSE das quais o JWS faz parte foram projetadas para serem fáceis de implementar, um recurso que também é um dos principais objetivos do projeto da roda. O JWS produz uma implementação puro Python concisa e útil.

Por que wheel também permite assinaturas S/MIME?

Assinaturas S/MIME são permitidas para usuários que precisam ou querem usar uma infraestrutura de chaves públicas existente com o wheel.

Pacotes assinados são apenas um bloco de construção básico em um sistema de atualização segura de pacotes. Wheel só fornece o bloco de construção.

Qual é a diferença entre “purelib” e “platlib”?

O Wheel preserva a distinção “purelib” vs. “platlib”, que é significativa em algumas plataformas. Por exemplo, o Fedora instala pacotes puro Python em ‘/usr/lib/pythonX.Y/site-packages’ e pacotes dependentes de plataforma em ‘/usr/lib64/pythonX.Y/site-packages’.

Um wheel com “Root-Is-Purelib: false” com todos os seus arquivos em {nome}-{versão}.data/purelib é equivalente a um wheel com “Root-Is-Purelib: true” com os mesmos arquivos na raiz, e é válido ter arquivos nas categorias “purelib” e “platlib”.

Na prática, um wheel deve ter apenas um de “purelib” ou “platlib” dependendo se é puro Python ou não e esses arquivos devem estar na raiz com a configuração apropriada fornecida para “Root-is-purelib”.

É possível importar código Python diretamente de um arquivo wheel?

Tecnicamente, devido à combinação de suporte de instalação via extração simples e usando um formato de arquivo compatível com zipimport, um subconjunto de arquivos wheel oferece suporte a ser colocado diretamente no sys.path. No entanto, embora esse comportamento seja uma consequência natural do design do formato, não é recomendável confiar nele.

Em primeiro lugar, o wheel é projetado principalmente como um formato de distribuição, então pular a etapa de instalação também significa evitar deliberadamente qualquer dependência de recursos que pressupõem a instalação completa (como ser capaz de usar ferramentas padrão como pip e virtualenv para capturar e gerenciar dependências de uma forma que possam ser devidamente rastreadas para fins de auditoria e atualização de segurança, ou integração completa com o maquinário de construção padrão para extensões C, publicando arquivos de cabeçalho no local apropriado).

Em segundo lugar, embora alguns softwares Python sejam escritos para suportar a execução direta de um arquivo zip, ainda é comum que o código seja escrito assumindo que foi totalmente instalado. Quando essa suposição é quebrada ao tentar executar o software a partir de um arquivo zip, as falhas podem frequentemente ser obscuras e difíceis de diagnosticar (especialmente quando ocorrem em bibliotecas de terceiros). As duas fontes mais comuns de problemas com isso são o fato de que a importação de extensões C de um arquivo zip não ser suportada pelo CPython (uma vez que fazer isso não é suportado diretamente pela máquina de carregamento dinâmico em qualquer plataforma) e que quando executando a partir de um arquivo zip, o atributo __file__ não se refere mais a um caminho de sistema de arquivos comum, mas a um caminho de combinação que inclui tanto a localização do arquivo zip no sistema de arquivos quanto o caminho relativo para o módulo dentro do arquivo. Mesmo quando o software usa corretamente as APIs de recursos abstratos internamente, a interface com componentes externos ainda pode exigir a disponibilidade de um arquivo real no disco.

Como metaclasses, monkeypatching e importadores de metacaminho, se você ainda não tem certeza de que precisa tirar proveito desse recurso, é quase certo que não precisa dele. Se você decidir usá-lo de qualquer maneira, esteja ciente de que muitos projetos exigirão que uma falha seja reproduzida com um pacote totalmente instalado antes de aceitá-lo como um bug genuíno.

Alterações

Desde a PEP 427, esta especificação mudou da seguinte forma:

  • As regras sobre como escapar em nomes de arquivos wheel foram revisadas, para alinhá-las com o que as ferramentas populares realmente fazem (fevereiro de 2021).

Referências

1

Aceitação da PEP (https://mail.python.org/pipermail/python-dev/2013-February/124103.html)

Apêndice

Exemplo de implementação de urlsafe-base64-nopad:

# urlsafe-base64-nopad for Python 3
import base64

def urlsafe_b64encode_nopad(data):
    return base64.urlsafe_b64encode(data).rstrip(b'=')

def urlsafe_b64decode_nopad(data):
    pad = b'=' * (4 - (len(data) & 3))
    return base64.urlsafe_b64decode(data + pad)