Vinculação dinâmica de aplicativos Qt no macOS

Hoje lancei uma versão do RaidenVideoRipper para dispositivos Apple com macOS e processadores M1/M2/M3/M4 (Apple Silicon). RaidenVideoRipper é um aplicativo rápido de edição de vídeo que permite cortar parte de um arquivo de vídeo em um novo arquivo. Você também pode criar gifs e exportar a trilha de áudio para mp3.

A seguir, descreverei brevemente quais comandos usei para fazer isso. A teoria do que está acontecendo aqui, a documentação das concessionárias, pode ser lida nos seguintes links:
https://www.unix.com/man-page/osx/1/otool/
https://www.unix.com/man-page/osx/1/install_name_tool/
https://llvm.org/docs/CommandGuide/llvm-nm.html
https://linux.die.net/man/1/file
https://www.unix.com/man-page/osx/8/SPCTL/
https://linux.die.net/man/1/chmod
https://linux.die.net/man/1/ls
https://man7.org/linux/man-pages/man7/xattr.7.html
https://doc.qt.io/qt-6/macos-deployment.html

Primeiro, instale o Qt no seu macOS e também instale o ambiente de desenvolvimento Qt Desktop. Depois disso, construa seu projeto, por exemplo, no Qt Creator. Em seguida, descreverei o que é necessário para que as dependências com bibliotecas dinâmicas externas funcionem corretamente ao distribuir o aplicativo para usuários finais.

Crie um diretório Frameworks na pasta YOUR_APP.app/Contents do seu aplicativo e coloque dependências externas nele. Por exemplo, esta é a aparência dos Frameworks para o aplicativo RaidenVideoRipper:

Frameworks
├── DullahanFFmpeg.framework
│   ├── dullahan_ffmpeg.a
│   ├── libavcodec.60.dylib
│   ├── libavdevice.60.dylib
│   ├── libavfilter.9.dylib
│   ├── libavformat.60.dylib
│   ├── libavutil.58.dylib
│   ├── libpostproc.57.dylib
│   ├── libswresample.4.dylib
│   └── libswscale.7.dylib
├── QtCore.framework
│   ├── Headers -> Versions/Current/Headers
│   ├── QtCore -> Versions/Current/QtCore
│   ├── Resources -> Versions/Current/Resources
│   └── Versions
├── QtGui.framework
│   ├── Headers -> Versions/Current/Headers
│   ├── QtGui -> Versions/Current/QtGui
│   ├── Resources -> Versions/Current/Resources
│   └── Versions
├── QtMultimedia.framework
│   ├── Headers -> Versions/Current/Headers
│   ├── QtMultimedia -> Versions/Current/QtMultimedia
│   ├── Resources -> Versions/Current/Resources
│   └── Versions
├── QtMultimediaWidgets.framework
│   ├── Headers -> Versions/Current/Headers
│   ├── QtMultimediaWidgets -> Versions/Current/QtMultimediaWidgets
│   ├── Resources -> Versions/Current/Resources
│   └── Versions
├── QtNetwork.framework
│   ├── Headers -> Versions/Current/Headers
│   ├── QtNetwork -> Versions/Current/QtNetwork
│   ├── Resources -> Versions/Current/Resources
│   └── Versions
└── QtWidgets.framework
    ├── Headers -> Versions/Current/Headers
    ├── QtWidgets -> Versions/Current/QtWidgets
    ├── Resources -> Versions/Current/Resources
    └── Versions

Para simplificar, imprimi apenas o segundo nível de aninhamento.

A seguir, imprimimos as dependências dinâmicas atuais da sua aplicação:

otool -L RaidenVideoRipper 

Saída para o binário RaidenVideoRipper, localizado em RaidenVideoRipper.app/Contents/MacOS:

RaidenVideoRipper:
	@rpath/DullahanFFmpeg.framework/dullahan_ffmpeg.a (compatibility version 0.0.0, current version 0.0.0)
	@rpath/QtMultimediaWidgets.framework/Versions/A/QtMultimediaWidgets (compatibility version 6.0.0, current version 6.8.1)
	@rpath/QtWidgets.framework/Versions/A/QtWidgets (compatibility version 6.0.0, current version 6.8.1)
	@rpath/QtMultimedia.framework/Versions/A/QtMultimedia (compatibility version 6.0.0, current version 6.8.1)
	@rpath/QtGui.framework/Versions/A/QtGui (compatibility version 6.0.0, current version 6.8.1)
	/System/Library/Frameworks/AppKit.framework/Versions/C/AppKit (compatibility version 45.0.0, current version 2575.20.19)
	/System/Library/Frameworks/ImageIO.framework/Versions/A/ImageIO (compatibility version 1.0.0, current version 1.0.0)
	/System/Library/Frameworks/Metal.framework/Versions/A/Metal (compatibility version 1.0.0, current version 367.4.0)
	@rpath/QtNetwork.framework/Versions/A/QtNetwork (compatibility version 6.0.0, current version 6.8.1)
	@rpath/QtCore.framework/Versions/A/QtCore (compatibility version 6.0.0, current version 6.8.1)
	/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit (compatibility version 1.0.0, current version 275.0.0)
	/System/Library/Frameworks/DiskArbitration.framework/Versions/A/DiskArbitration (compatibility version 1.0.0, current version 1.0.0)
	/System/Library/Frameworks/UniformTypeIdentifiers.framework/Versions/A/UniformTypeIdentifiers (compatibility version 1.0.0, current version 709.0.0)
	/System/Library/Frameworks/AGL.framework/Versions/A/AGL (compatibility version 1.0.0, current version 1.0.0)
	/System/Library/Frameworks/OpenGL.framework/Versions/A/OpenGL (compatibility version 1.0.0, current version 1.0.0)
	/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 1800.101.0)
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1351.0.0)

Como pode ser visto no RaidenVideoRipper nas dependências Qt e Dullahan_ffmpeg. Dullahan FFmpeg é um fork do FFmpeg que encapsula sua funcionalidade em uma biblioteca dinâmica, com a capacidade de obter o progresso e cancelamento da execução atual usando rotinas C.
Em seguida, substitua os caminhos do aplicativo e todas as bibliotecas necessárias usando install_name_tool.

O comando para isso é:

install_name_tool -change old_path new_path target

Exemplo de uso:

install_name_tool -change /usr/local/lib/libavfilter.9.dylib @rpath/DullahanFFmpeg.framework/libavfilter.9.dylib dullahan_ffmpeg.a

Depois de inserir todos os caminhos corretos, o aplicativo deverá iniciar corretamente. Verifique se todos os caminhos para bibliotecas são relativos, transfira o binário e abra-o novamente.
Se você encontrar algum erro, verifique os caminhos via otool e altere novamente via install_name_tool.

Também ocorre um erro com confusão de dependências, quando a biblioteca que você substituiu não possui um símbolo na tabela você pode verificar a presença ou ausência de um símbolo assim:

nm -gU path

Uma vez executado, você verá toda a tabela de símbolos da biblioteca ou aplicativo.
Também é possível que você tenha copiado as dependências da arquitetura errada, você pode verificar isso usando o arquivo:

file path

O utilitário de arquivo mostrará a qual arquitetura a biblioteca ou aplicativo pertence.

O Qt também requer uma pasta Plugins na pasta Contents do seu diretório YOUR_APP.app, copie os plugins do Qt para Contents. A seguir, verifique a funcionalidade do aplicativo, após o qual você poderá começar a otimizar a pasta Plugins, excluindo elementos desta pasta e testando o aplicativo.

Segurança do macOS

Depois de copiar todas as dependências e corrigir os caminhos para vinculação dinâmica, você precisará assinar o aplicativo com a assinatura do desenvolvedor e, adicionalmente, enviar a versão do aplicativo à Apple para reconhecimento de firma.

Se você não tem US$ 100 para uma licença de desenvolvedor ou não deseja assinar nada, escreva instruções para seus usuários sobre como iniciar o aplicativo.

Esta instrução também funciona para RaidenVideoRipper:

  • Desativar o Gatekeeper: spctl –master-disable
  • Permitir inicialização de qualquer fonte em Privacidade e segurança: permitir que aplicativos mudem para qualquer lugar
  • Remova o sinalizador de quarentena após fazer download de um aplicativo zip ou dmg: xattr -d com.apple.quarantine app.dmg
  • Verifique se o sinalizador de quarentena (com.apple.quarantine) está faltando: ls -l@ app.dmg
  • Adicione confirmação para iniciar o aplicativo, se necessário, em Privacidade e segurança

Um erro com o sinalizador de quarentena geralmente é reproduzido pelo erro “O aplicativo está corrompido” que aparece na tela do usuário. Nesse caso, você precisa remover o sinalizador de quarentena dos metadados.

Link para construir o RaidenVideoRipper para Apple Silicon:
https://github.com/demensdeum/RaidenVideoRipper/releases/download/1.0.1.0/RaidenVideoRipper-1.0.1.0.dmg

Leave a Comment

Your email address will not be published. Required fields are marked *