Crear una herramienta de inmediato para que pueda usarse más tarde en la misma ejecución de CMake
(1)
Tengo un problema interesante de huevo y gallina y una posible solución (vea mi respuesta publicada), pero esa solución usa CMake de una manera inusual. Mejores alternativas o comentarios serían bienvenidos.
EL PROBLEMA:
La versión simple del problema se puede describir como un solo proyecto CMake con las siguientes características:
-
Uno de los objetivos de compilación es un ejecutable de línea de comandos que llamaré
mycomp
,
cuya
fuente está en
mycompdir
y no es posible realizar modificaciones en el contenido de ese directorio. -
El proyecto contiene archivos de texto (los llamaré
foo.my
ybar.my
) que necesitan que mycomp se ejecute en ellos para producir un conjunto de fuentes y encabezados de C ++ y algunos archivosCMakeLists.txt
que definen bibliotecas creadas a partir de esas fuentes. -
Otros objetivos de compilación en el mismo proyecto deben vincularse con las bibliotecas definidas por los archivos
CMakeLists.txt
generados. Estos otros objetivos también tienen fuentes que#include
algunos de los encabezados generados.
Puede pensar en
mycomp
como algo así como un compilador y los archivos de texto en el paso 2 como algún tipo de archivos fuente.
Esto presenta un problema, porque CMake necesita los archivos
CMakeLists.txt
en el momento de la configuración, pero
mycomp
no está disponible hasta el momento de la compilación y, por lo tanto, no está disponible en la primera ejecución para crear los archivos
CMakeLists.txt
suficiente antelación.
NO RESPUESTA
Normalmente, un arreglo de superconstrucción basado en proyecto externo sería una solución potencial para esto, pero lo anterior es una simplificación considerable del proyecto real con el que estoy tratando y no tengo la libertad de dividir la construcción en diferentes partes o realizar otras grandes Trabajos de reestructuración a escala.
El meollo del problema es que
mycomp
esté disponible cuando se ejecute CMake para que los archivos
CMakeLists.txt
generados puedan crearse y luego
add_subdirectory()
con
add_subdirectory()
.
Una posible forma de lograr esto es usar
execute_process()
para ejecutar un cmake-and-build anidado desde la compilación principal.
Ese cmake-and-build anidado usaría exactamente los mismos directorios fuente y binarios que la ejecución CMake de nivel superior (a menos que se realice una compilación cruzada).
La estructura general del
CMakeLists.txt
principal de nivel
CMakeLists.txt
sería algo como esto:
# Usual CMakeLists.txt setup stuff goes here...
if(EARLY_BUILD)
# This is the nested build and we will only be asked to
# build the mycomp target (see (c) below)
add_subdirectory(mycompdir)
# End immediately, we don''t want anything else in the nested build
return()
endif()
# This is the main build, setup and execute the nested build
# to ensure the mycomp executable exists before continuing
# (a) When cross compiling, we cannot re-use the same binary dir
# because the host and target are different architectures
if(CMAKE_CROSSCOMPILING)
set(workdir "${CMAKE_BINARY_DIR}/host")
execute_process(COMMAND ${CMAKE_COMMAND} -E make_directory "${workdir}")
else()
set(workdir "${CMAKE_BINARY_DIR}")
endif()
# (b) Nested CMake run. May need more -D... options than shown here.
execute_process(COMMAND ${CMAKE_COMMAND} -G "${CMAKE_GENERATOR}"
-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
-DCMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM}
-DEARLY_BUILD=ON
${CMAKE_SOURCE_DIR}
WORKING_DIRECTORY "${workdir}")
# (c) Build just mycomp in the nested build. Don''t specify a --config
# because we cannot know what config the developer will be using
# at this point. For non-multi-config generators, we''ve already
# specified CMAKE_BUILD_TYPE above in (b).
execute_process(COMMAND ${CMAKE_COMMAND} --build . --target mycomp
WORKING_DIRECTORY "${workdir}")
# (d) We want everything from mycompdir in our main build,
# not just the mycomp target
add_subdirectory(mycompdir)
# (e) Run mycomp on the sources to generate a CMakeLists.txt in the
# ${CMAKE_BINARY_DIR}/foobar directory. Note that because we want
# to support cross compiling, working out the location of the
# executable is a bit more tricky. We cannot know whether the user
# wants debug or release build types for multi-config generators
# so we have to choose one. We cannot query the target properties
# because they are only known at generate time, which is after here.
# Best we can do is hardcode some basic logic.
if(MSVC)
set(mycompsuffix "Debug/mycomp.exe")
elseif(CMAKE_GENERATOR STREQUAL "Xcode")
set(mycompsuffix "Debug/mycomp")
else()
set(mycompsuffix "mycomp")
endif()
set(mycomp_EXECUTABLE "${workdir}/mycompdir/${mycompsuffix}")
execute_process(COMMAND "${mycomp_EXECUTABLE}" -outdir foobar ${CMAKE_SOURCE_DIR}/foo.my ${CMAKE_SOURCE_DIR}/bar.my)
# (f) Now pull that generated CMakeLists.txt into the main build.
# It will create a CMake library target called foobar.
add_subdirectory(${CMAKE_BINARY_DIR}/foobar ${CMAKE_BINARY_DIR}/foobar-build)
# (g) Another target which links to the foobar library
# and includes headers from there
add_executable(gumby gumby.cpp)
target_link_libraries(gumby PUBLIC foobar)
target_include_directories(gumby PUBLIC foobar)
Si no reutilizamos el mismo directorio binario en (b) y (c) que usamos para la compilación principal, terminamos
mycomp
dos veces, lo que obviamente queremos evitar.
Para la compilación cruzada, no podemos evitar eso, por lo que en tales casos construimos la herramienta
mycomp
a un lado en un directorio binario separado.
He experimentado con el enfoque anterior y, de hecho, parece funcionar en el proyecto del mundo real que provocó la pregunta original, al menos para los generadores Unix Makefiles, Ninja, Xcode (OS X e iOS) y los generadores de Visual Studio.
Parte del atractivo de este enfoque es que solo requiere que se agregue una cantidad modesta de código solo al archivo
CMakeLists.txt
nivel superior.
Sin embargo, hay algunas observaciones que deben hacerse:
-
Si los comandos del compilador o enlazador para
mycomp
y sus fuentes son diferentes de alguna manera entre la compilación anidada y la compilación principal, el objetivo demycomp
termina siendo reconstruido por segunda vez en (d). Si no hay diferencias,mycomp
solo semycomp
una vez cuando no se realiza la compilación cruzada, que es exactamente lo que queremos. -
No veo una manera fácil de pasar exactamente los mismos argumentos a la invocación anidada de CMake en (b) que se pasó a la ejecución de CMake de nivel superior (básicamente el problema descrito
here
).
Leer
CMakeCache.txt
no es una opción, ya que no existirá en la primera invocación y, de todos modos, no le dará argumentos nuevos o modificados de la ejecución actual. Lo mejor que puedo hacer es establecer esas variables CMake que creo que potencialmente se usarán y que pueden influir en los comandos del compilador y el enlazador demycomp
. Esto se puede solucionar agregando más y más variables a medida que encuentro las que descubro que necesito, pero eso no es lo ideal. - Cuando reutilizamos el mismo directorio binario, confiamos en que CMake no comience a escribir ninguno de sus archivos en el directorio binario hasta la etapa de generación (bueno, al menos hasta que se complete la compilación en (c)). Para los generadores probados, parece que estamos bien, pero no sé si todos los generadores en todas las plataformas también siguen este comportamiento (¡y no puedo probar cada combinación para descubrirlo!). Esta es la parte que me preocupa más. Si alguien puede confirmar con razonamiento y / o evidencia que esto es seguro para todos los generadores y plataformas, sería valioso (y vale la pena votar si desea abordar esto como una respuesta por separado).
ACTUALIZACIÓN: Después de utilizar la estrategia anterior en una serie de proyectos del mundo real con personal de diferentes niveles de familiaridad con CMake, se pueden hacer algunas observaciones.
-
Hacer que la compilación anidada reutilice el mismo directorio de compilación que la compilación principal ocasionalmente puede generar problemas. Específicamente, si un usuario mata la ejecución de CMake después de que se completa la compilación anidada pero antes de que lo haga la compilación principal, el archivo
CMakeCache.txt
se deja conEARLY_BUILD
configurado enON
. Esto hace que todas las ejecuciones posteriores de CMake actúen como una compilación anidada, por lo que la compilación principal se pierde esencialmente hasta que el archivoCMakeCache.txt
se elimina manualmente. Es posible que un error en alguna parte del archivoCMakeLists.txt
del proyecto también pueda conducir a una situación similar (sin confirmar). Realizar la compilación anidada a un lado en su propio directorio de compilación separado ha funcionado muy bien, aunque sin tales problemas. -
La compilación anidada probablemente debería ser Release en lugar de Debug . Si no reutiliza el mismo directorio de compilación que la compilación principal (ahora lo que recomendaría), ya no nos importa tratar de evitar compilar el mismo archivo dos veces, por lo que también podría hacer que mycomp sea lo más rápido posible.
-
Use ccache para minimizar los costos debidos a la reconstrucción de algunos archivos dos veces con diferentes configuraciones. En realidad, descubrimos que usar ccache normalmente hace que la compilación anidada sea muy rápida, ya que rara vez cambia en comparación con la compilación principal.
-
La compilación anidada probablemente necesite tener
CMAKE_BUILD_WITH_INSTALL_RPATH
establecido enFALSE
en algunas plataformas para que pueda encontrar cualquier biblioteca que necesite micomp sin tener que establecer variables de entorno, etc.