Proper way to craft a CMakeLists.txt whose (only) output is a Bash script?

I have a project using ANTLR4 with an executable in a top level CMakeLists.txt, and one of the subfolders is “testrig” which I want to just produce a Bash script that issues Java commandlines to run the ANTLR4 testrig on my grammar. (The reason it can’t be a static script file is there are substitutions for the name of the grammar and path of the CMake build output for compiled artifacts produced from the grammar file.)

Previously I had written CMake code to run those Java commands during configuration step, but that required re-running configuration for that target. I changed things so that testrig builds a tool with add_executable, but that program just used a process-start C++ library to run commands based on settings written to an .ini file generated during CMake configuration. I thought, well, this is kinda pointless pulling in a library and building an executable to do what could be just a Bash script (run by MSys2 in my case).

It seems like my “testrig/CMakeLists.txt” file could be very simple, but I’m finding it surprisingly difficult to search the web for an example of this specific use-case and only this. Everything I’ve found is either relevant to generating a file but not having a file as a target, or else does other things like add_executable.

The problem I have with the example in this link is it uses add_executable and thus isn’t different from the intermediate way I’ve already done it with a process-starting executable:
https://cmake.org/cmake/help/latest/guide/tutorial/Adding%20a%20Custom%20Command%20and%20Generated%20File.html

The problem I have with this next example (add_custom_target) is it says the custom target will be considered always out of date. Does that mean it will rebuild the script every time even if it’s not needed to be rebuilt?
https://cmake.org/cmake/help/v3.0/command/add_custom_target.html

I am sure that somehow add_custom_command and add_custom_target will be involved in the solution; I’m just not quite seeing things clearly. I’m using CMake “file(WRITE …” to create the Bash script, so I’m not understanding what there is for add_custom_command to do here. Do I have to change from “file(WRITE …” to “echo” commands so I can write the .sh file at build time, or can I have add_custom_target with DEPENDS on the output of “file(WRITE …” with no add_custom_command?

Actually that’s not entirely true; I would still have “add_custom_command” to generate the Java .class files needed by the testrig script target. Previously I had problems I never fully figured out with those custom commands getting run more times than I expected. I assume either it was because if I had multiple outputs from the command it was re-running the command for each output (instead of understanding that one command produced two files), and/or the always-out-of-date thing I am worried about for add_custom_target.

So to recap, first attempt looked like this:
add_custom_command → .class file
add_custom_command → runs ANTLR4 testrig
add_custom_target ← depends on both custom commands

Second attempt:
add_custom_command → .class file
file(WRITE …) → .ini file
add_executable(testrig_runner) ← depends on outputs above

Third attempt:
add_custom_command → .class file
file(WRITE …) → .sh file
<<< what goes here? >>>

Hi. Can you please clarify when & how you want the script to be executed? There’s nothing wrong with creating a script using file(WRITE), which you can apparently do now. It’s not clear to me what is missing. Please elaborate on the state you want to achieve: what should get created at CMake time, what should get created at build time, what build targets (=make what_ever commands) should be available etc.


If the answers are “the script can be created whenever, the .class files should be created at build time, and there should be a make run_script target that runs the script if and only if invoked explicitly,” then you could achieve that like this:

add_custom_command(
  OUTPUT wherever/whatever.class 
  COMMAND somehow do it
  COMMENT "Compiling whatever.class"
  DEPENDS all/sources.java which/should/cause/rebuild.java
  VERBATIM
  # ... other args as necessary
)
file(WRITE somewhere/script.sh "script contents")
add_custom_target(run_script
  COMMAND sh somewhere/script.sh
  COMMENT "Running test script"
  DEPENDS wherever/whatever.class
  VERBATIM
  # ... other args as necessary
)

This will generate build rules to create the .class file and create a build target called run_script that:

  • is not part of make all (there was no ALL argument in the add_custom_target())
  • depends on the .class file, i.e. causes the file to be built if it is absent or out of date
  • always execute the script when invoked (via e.g. make run_script)

If what you want differs from the above, please specify how and we can look for a solution.

What I want is to be able to execute the script from the Run/Debug menu in CLion (as if it were an EXE generated by add_executable). As a custom target, it does show up in CLion’s drop down of targets, but CLion won’t run non-executable targets, such as scripts:

https://youtrack.jetbrains.com/issue/CPP-5831

However (and as I commented on that issue), I did find a work-around. Since I have Msys2 installed, I can specify msys2.exe as the Executable for CLion to run, and the script as an argument.

One thing this has made me realize is that although the script has a runtime dependency on build outputs, it doesn’t actually have a build dependency on it. In fact, the .sh file can be (and is) written during the configuration step. To my understanding, CMake doesn’t really have a way to express purely runtime dependencies:

…but that’s moot in this case as the script doesn’t take any build-time time. (What does get built at build time is custom commands used to compile the Java code for the grammar, which the .sh script then has a runtime dependency on.)

I believe the example I gave covers all of your points:

  • It uses the shell as the command executable (I used sh in the example, you’d apparently be using msys2.exe).
  • It introduces dependencies such that the custom commands will be checked for staleness and built if necessary before the script is run.