Usando `make` para compilar programas C (para não programadores C)


Nunca fui um programador C, mas de vez em quando preciso compilar um programa C/C++ a partir do código-fonte. Isso tem sido uma espécie de luta para mim: por muito tempo, minha abordagem foi basicamente “instalar as dependências, executar makese não funcionar, tente encontrar um binário que alguém compilou ou desista”.

“Espero que alguém tenha compilado” funcionou muito bem quando eu estava executando o Linux, mas desde que comecei a usar um Mac nos últimos anos, tenho me deparado com mais situações em que tenho que compilar programas sozinho.

Então, vamos falar sobre o que você precisa fazer para compilar um programa em C! Usarei alguns exemplos de programas C específicos que compilei e falarei sobre algumas coisas que podem dar errado. Aqui estão três programas sobre os quais falaremos sobre compilação:

  • atolamento de papel
  • sqlite
  • qf (um pager que você pode executar para abrir arquivos rapidamente a partir de uma pesquisa com rg -n THING | qf)

etapa 1: instale um compilador C

Isso é bem simples: em um sistema Ubuntu, se eu ainda não tiver um compilador C, instalarei um com:

sudo apt-get install build-essential

Isso instala gcc, g++e make. A situação em um Mac é mais confusa, mas é algo como “instalar ferramentas de linha de comando xcode”.

passo 2: instale as dependências do programa

Ao contrário de algumas linguagens de programação mais recentes, C não possui um gerenciador de dependências. Portanto, se um programa tiver alguma dependência, você mesmo precisará procurá-la. Felizmente, por causa disso, os programadores C geralmente mantêm suas dependências mínimas e muitas vezes as dependências estarão disponíveis em qualquer gerenciador de pacotes que você estiver usando.

Quase sempre há uma seção explicando como obter as dependências no README, por exemplo, no README do paperjam, que diz:

Para compilar o PaperJam, você precisa dos cabeçalhos das bibliotecas libqpdf e libpaper (geralmente disponíveis como pacotes libqpdf-dev e libpaper-dev).

Você pode precisar a2x (encontrado em AsciiDoc) para construir páginas de manual.

Portanto, em um sistema baseado em Debian você pode instalar as dependências como esta.

sudo apt install -y libqpdf-dev libpaper-dev

Se um README fornecer um nome para um pacote (como libqpdf-dev), eu basicamente sempre presumo que eles significam “em uma distribuição Linux baseada em Debian”: se você estiver em um Mac brew install libqpdf-dev não funcionará. Ainda não aprendi 100% a desenvolver em um Mac, então não tenho muitas dicas sobre isso ainda. Acho que neste caso seria brew install qpdf se você estiver usando o Homebrew.

passo 3: executar ./configure (se necessário)

Alguns programas C vêm com um Makefile e alguns vêm com um script chamado
./configure. Por exemplo, se você baixar o código-fonte do sqlite, ele terá um ./configure script nele em vez de um Makefile.

Minha compreensão disso ./configure roteiro é:

  1. Você o executa, ele imprime uma série de resultados um tanto inescrutáveis ​​e então gera um Makefile ou falha porque está faltando alguma dependência
  2. O ./configure script faz parte de um sistema chamado autotools sobre o qual nunca precisei aprender nada além de “executá-lo para gerar um Makefile”.

Acho que pode haver algumas opções que você pode passar para obter o ./configure
roteiro para produzir um diferente Makefile mas nunca fiz isso.

passo 4: executar make

O próximo passo é executar make para tentar construir um programa. Algumas notas sobre
make:

  • Às vezes você pode correr make -j8 para paralelizar a construção e torná-la mais rápida
  • Geralmente imprime um milhão de avisos do compilador ao compilar o programa. Eu sempre os ignoro. Eu não escrevi o software! Os avisos do compilador não são problema meu.

erros do compilador são frequentemente problemas de dependência

Aqui está um erro que recebi durante a compilação paperjam no meu Mac:

/opt/homebrew/Cellar/qpdf/12.0.0/include/qpdf/InputSource.hh:85:19: error: function definition does not declare parameters
   85 |     qpdf_offset_t last_offset{0};
      |                   ^

Ao longo dos anos, aprendi que geralmente é melhor não pensar demais em problemas como este: se estiver falando sobre qpdfhá uma boa mudança, significa apenas que fiz algo errado na forma como estou incluindo o qpdf dependência.

Agora vamos falar sobre algumas maneiras de obter o qpdf dependência incluída da maneira certa.

a introdução mais curta do mundo ao compilador e vinculador

Antes de falarmos sobre como corrigir problemas de dependência: a construção de programas C é dividida em 2 etapas:

  1. Compilando o código em arquivos de objeto (com gcc ou clang)
  2. Vinculando esses arquivos objeto em um binário final (com ld)

É importante saber isso ao construir um programa C porque às vezes você precisa passar os sinalizadores corretos para o compilador e vinculador para informar onde encontrar as dependências do programa que você está compilando.

make usa variáveis ​​de ambiente para configurar o compilador e o vinculador

Se eu correr make no meu Mac para instalar paperjamrecebo este erro:

c++ -o paperjam paperjam.o pdf-tools.o parse.o cmds.o pdf.o -lqpdf -lpaper
ld: library 'qpdf' not found

Isto não é porque qpdf não está instalado no meu sistema (na verdade está!). Mas o compilador e o vinculador não sabem como encontrar o qpdf biblioteca. Para corrigir isso, precisamos:

  • passar "-I/opt/homebrew/include" para o compilador (para informar onde encontrar os arquivos de cabeçalho)
  • passar "-L/opt/homebrew/lib -liconv" ao vinculador (para dizer onde encontrar os arquivos da biblioteca e vincular iconv)

E podemos conseguir make para passar esses parâmetros extras para o compilador e vinculador usando variáveis ​​de ambiente! Para ver como isso funciona: dentro paperjamNo Makefile você pode ver um monte de variáveis ​​de ambiente, como LDLIBS aqui:

paperjam: $(OBJS)
	$(LD) -o $@ $^ $(LDLIBS)

Tudo o que você coloca no LDLIBS variável de ambiente é passada para o vinculador (ld) como um argumento de linha de comando.

variável de ambiente secreta: CPPFLAGS

Makefiles às vezes definem suas próprias variáveis ​​de ambiente que passam para o compilador/vinculador, mas make também possui um monte de variáveis ​​de ambiente “implícitas” que serão passadas automaticamente para o compilador e vinculador C. Há uma lista completa de variáveis ​​de ambiente implícitas aqui, mas uma delas é CPPFLAGSque é passado automaticamente para o compilador C.

(tecnicamente seria mais normal usar CXXFLAGS para isso, mas este particular Makefile códigos fixos CXXFLAGS então definindo CPPFLAGS foi a única maneira que encontrei de definir os sinalizadores do compilador sem editar o Makefile)

Como um aparte: levei muito tempo para perceber o quão intimamente ligado ao C/C++ `make` está – eu costumava pensar que `make` era apenas um sistema de construção geral (e é claro que você pode usá-lo para qualquer coisa!), mas ele tem muitas possibilidades para construir programas C/C++ que não tem para construir qualquer outro tipo de programa.

duas maneiras de passar variáveis ​​de ambiente para make

Aprendi graças ao @zwol que na verdade existem duas maneiras de passar variáveis ​​de ambiente para make:

  1. CXXFLAGS=xyz make (da maneira usual)
  2. make CXXFLAGS=xyz

A diferença entre eles é que make CXXFLAGS=xyz substituirá o valor de CXXFLAGS definido no Makefile mas CXXFLAGS=xyz make não vai.

Não tenho certeza de qual caminho é a norma, mas vou usar o primeiro caminho neste post.

como usar CPPFLAGS e LDLIBS para corrigir este erro do compilador

Agora que falamos sobre como CPPFLAGS e LDLIBS for passado para o compilador e vinculador, aqui está o encantamento final que usei para fazer o programa ser compilado com sucesso!

CPPFLAGS="-I/opt/homebrew/include" LDLIBS="-L/opt/homebrew/lib -liconv" make paperjam

Isso passa -I/opt/homebrew/include para o compilador e -L/opt/homebrew/lib -liconv para o vinculador.

Além disso, não quero fingir que sabia “magicamente” que esses eram os argumentos certos a serem apresentados, descobri-los envolveu um monte de pesquisas confusas no Google que pulei neste post. Eu direi isso:

  • o -I sinalizador do compilador informa ao compilador em qual diretório encontrar os arquivos de cabeçalho, como /opt/homebrew/include/qpdf/QPDF.hh
  • o -L sinalizador do vinculador informa ao vinculador em qual diretório encontrar bibliotecas, como /opt/homebrew/lib/libqpdf.a
  • o -l sinalizador do vinculador informa ao vinculador quais bibliotecas vincular, como -liconv significa “link no iconv biblioteca”, ou -lm significa “ligação math”

dica: como construir apenas 1 arquivo específico: make $FILENAME

Ontem eu descobri uma ferramenta legal chamada qf que você pode usar para abrir arquivos rapidamente a partir da saída do ripgrep.

qf está em um grande diretório de várias ferramentas, mas eu só queria compilar qf. Então eu acabei de compilar qfassim:

make qf

Basicamente, se você souber (ou puder adivinhar) o nome do arquivo de saída que está tentando construir, poderá saber make apenas construir esse arquivo executando make $FILENAME

dica: você não precisa de um Makefile

Às vezes escrevo programas C de 5 linhas sem dependências e acabei de aprender que se eu tiver um arquivo chamado blah.cposso simplesmente compilá-lo assim sem criar um Makefile:

make blah

Ele é automaticamente expandido para cc -o blah blah.co que economiza um pouco de digitação. Não tenho ideia se vou me lembrar disso (talvez continue digitando
gcc -o blah blah.c de qualquer maneira), mas parece um truque divertido.

dica: veja como outros sistemas de empacotamento construíram o mesmo programa C

Se você está tendo problemas para construir um programa em C, talvez outras pessoas também tenham tido problemas para construí-lo! Cada distribuição Linux possui arquivos de construção para cada pacote que eles constroem, então mesmo que você não possa instalar pacotes dessa distribuição diretamente, talvez você possa obter dicas dessa distribuição Linux sobre como construir o pacote. Perceber isso (graças ao meu amigo Dave) foi um grande momento de ah-ha para mim.

Por exemplo, esta linha do pacote nix para paperjam diz:

  env.NIX_LDFLAGS = lib.optionalString stdenv.hostPlatform.isDarwin "-liconv";

Isso basicamente diz “passar o sinalizador do vinculador -liconv para construir isso em um Mac”, então essa é uma pista que poderíamos usar para construí-lo.

Esse mesmo arquivo também diz env.NIX_CFLAGS_COMPILE = "-DPOINTERHOLDER_TRANSITION=1";. Não tenho certeza do que isso significa, mas quando tento construir o paperjam pacote, recebo um erro sobre algo chamado
PointerHolderentão acho que isso está de alguma forma relacionado à “transição PointerHolder”.

etapa 5: instalando o binário

Depois de conseguir compilar o programa, provavelmente você desejará instalá-lo em algum lugar! Alguns Makefiletem um install alvo que permite instalar a ferramenta em seu sistema com make install. Sempre fico com um pouco de medo disso (onde os arquivos serão colocados? e se eu quiser desinstalá-los mais tarde?), então, se estou compilando um programa bem simples, geralmente copio manualmente o binário para instalá-lo, assim:

cp qf ~/bin

passo 6: talvez faça seu próprio pacote!

Depois que descobri como fazer tudo isso, percebi que poderia usar meu novo
make conhecimento para contribuir paperjam pacote para Homebrew! Então eu poderia simplesmente brew install paperjam em sistemas futuros.

O bom é que, mesmo que sejam detalhados todos os diferentes sistemas de empacotamento, todos eles usam fundamentalmente compiladores e linkers C.

pode ser útil entender um pouco sobre C mesmo se você não for um programador C

Acho que tudo isso é um exemplo interessante de como pode ser útil entender alguns princípios básicos de como os programas C funcionam (como “eles têm arquivos de cabeçalho”), mesmo que você nunca planeje escrever um programa C não trivial em sua vida.

É bom ter alguma habilidade para compilar programas C/C++ sozinho, embora eu ainda não esteja totalmente confiante sobre todos os sinalizadores do compilador e do vinculador e ainda pretendo nunca aprender nada sobre como as ferramentas automáticas funcionam além de “você executa ./configure para gerar o Makefile”.

Duas coisas que deixei de fora deste post:

  • LD_LIBRARY_PATH / DYLD_LIBRARY_PATH (que você usa para informar ao vinculador dinâmico em tempo de execução onde encontrar arquivos vinculados dinamicamente) porque não consigo me lembrar da última vez que encontrei um LD_LIBRARY_PATH problema e não consegui encontrar um exemplo.
  • pkg-configo que acho importante, mas ainda não entendi



Source link

Deixe um comentário

O seu endereço de email não será publicado. Campos obrigatórios marcados com *

Botão Voltar ao Topo