0

I am trying to convert our installation process to use cmake instead of our old custom build that we have today. In the project we do have a few perl files that sometimes change that we want to include in the built product. In these perl files we want to set the shebang (#!/path/to/perl) during installation, so it can run on the system where cmake is run.

Usually I would use configure_file(), but due to the nature of configure_file() any @Unidentified-sequence@ will default to an empty string, when using configure_file() with any other option than COPYONLY (https://gitlab.kitware.com/cmake/cmake/-/issues/22740). This becomes inconvenient for perl scripts, as @ is used to define an array.

The workaround I have today is to read the file into cmake and use string replace,

file(READ "${srcDir}/${file}"  FILE_CONTENTS)
#I have full control over the files. Thus adding a custom variable, such as @PERLPATH@, instead of a REGEX, is possible
string(REGEX REPLACE "#!/[/A-Za-z_0-9.-]+/perl" "#!${PERL_PATH}" FILE_CONTENTS "${FILE_CONTENTS}")
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${destDir}/${file}" CONTENT "${FILE_CONTENTS}")

This works, but any changes to the perl files requires cmake -B /path/to/build -S /path/to/source to be rerun. Also, it will rerun this for any file and not only the changed files.

I would like it to automatically update the files when doing cmake --build. Further, I would also prefer to have a check whether the files are changed or not and only update them then (which requires old change data to be saved and thus cannot be done during configure, cmake -B /path/to/build -S /path/to/source).

I would appreciate if someone had a standardized solution to this, but as I understand it, this use case is not exactly supported in CMake. Thus I would consider it good enough with a workaround. Getting the change date is easy enough with stat -c %y.

Is there any way to force cmake to run some particular cmake code snippet during cmake --build, lets say the above example?

starball
  • 20,030
  • 7
  • 43
  • 238
patrik
  • 4,506
  • 6
  • 24
  • 48
  • If you want to automatically re-run configuration stage if your template changes, then see [that question](https://stackoverflow.com/questions/24246037/how-to-make-cmake-reconfiguration-depend-on-custom-file). Note, that it is not possible to re-run configuration stage *partially*: the configuration is indivisible. If you want to only to regenerate a specific file, then use `add_custom_command`/`add_custom_target` approach. Within `COMMAND` you may specify any script which could perform you task. You could even write a CMake script and run it with `cmake -P – Tsyvarev Feb 23 '23 at 18:19

2 Answers2

1
  1. use find_program() to find perl and define e.g., PERL_EXECUTABLE
  2. if perl is not found (dunno how critical it's for you) set PERL_EXECUTABLE to a default value, e.g., /usr/bin/perl, so configure_file() will always substitute #!@PERL_EXECUTABLE@ in shebang to smth not empty
  3. use add_custom_command(<fixed-shebang>.perl...) to run cmake -P <your-script-with-configure_file>.cmake [<input-files>] to produce the perl file(s) to install from their "templates"
  4. use install(<FILES|PROGRAMS> ${CMAKE_BINARY_DIR}/<fixed-shebang>.perl) command to install 'em to a desired location

This way edit files will "rebuild" 'em on cmake --build … and install fixed shebang versions on cmake --install.

Update:

According to the substitution problem, I can't reproduce:

  1. I pick a random Perl module from my system (e.g., Storable.pm where @ symbols exist) and copy it to a test directory as Storable.pm.in
  2. add #!@PERL_EXECUTABLE@ as the very first line in it
  3. added a subst.cmake to the test directory with the following content:
message(STATUS "PERL_EXECUTABLE=${PERL_EXECUTABLE}")
configure_file(Storable.pm.in Storable.pm @ONLY)
  1. run cmake -DPERL_EXECUTABLE=/usr/bin/perl -P subst.cmake
  2. and then diff Storable.pm.in Storable.pm:
--- Storable.pm.in      2023-02-24 15:10:18.630116604 +0400
+++ Storable.pm 2023-02-24 15:12:00.587358711 +0400
@@ -1,4 +1,4 @@
-#!@PERL_EXECUTABLE@
+#!/usr/bin/perl
 #
 #  Copyright (c) 1995-2001, Raphael Manfredi
 #  Copyright (c) 2002-2014 by the Perl 5 Porters
zaufi
  • 6,811
  • 26
  • 34
  • Thank you for your answer, but when it comes to (2), it will not work (or possibly work too well, depending on point of view). Perl itself uses @ to declare an array. It can look like this `@unique_object = grep { ! $temp{$_}++ } @Object_array;`, where configure_file() will replace `unique_object = grep { ! $temp{$_}++ }` with an empty string. This is the root of the problem. – patrik Feb 23 '23 at 17:34
  • @patrik I've updated my answer w/ more details about substitition – zaufi Feb 24 '23 at 11:17
  • A normal sum would break it, ´my $sum = 2+@t1+@t2;´. It will be replaced with `my $sum = 2+t2;`. Unfortunately the comment field is not useful for posting execution results, so you would have to try it yourself :(. Just adding a space between the operators would solve the issue in this case `my $sum = 2 + @t1 + @t2;`. However, I do not really trust a solution that might break for bad style, even if the language rules are still followed. – patrik Feb 27 '23 at 09:50
  • @patrik, I don't know Perl, is the other form of variable interpolation legal in it? I mean `${PERL_EXECUTABLE}` is valid Perl? – zaufi Feb 27 '23 at 22:59
1

If the output file of this procedure is needed as a source file for any targets, use add_custom_command. Otherwise, use add_custom_target.

Since you've already written some CMake to perform the task, just put that CMake in its own .cmake file and run it in the custom command/target using "${CMAKE_COMMAND}" -P <path to that script file>. (see docs on -P here).

In the DEPENDS field, specify the path to the CMake script and the path to any input files that are required by the CMake script. You will need to pass the value of CMAKE_CURRENT_BINARY_DIR to the CMake script in the command field with "-DCMAKE_CURRENT_BINARY_DIR=${CMAKE_CURRENT_BINARY_DIR}", since the script will not automatically have the same variables as the project that calls it (see docs on script-mode's -D flag here).

If the output file is needed as a source file for any targets (the add_custom_command route), then specify its path in the OUTPUT field, and then the custom command will be run as needed any time a target that needs that output file gets built. In the add_custom_target route, custom targets are always run, so if you want to have fancy logic that skips running if the input files haven't changed, I think you'll need to implement that in the script file itself, which you can do by using the if(file1 IS_NEWER_THAN file2) command, and file(TOUCH) to create a file with the timestamp of when the custom target body was last run (so if the touch file is newer than all the input files, just return).

starball
  • 20,030
  • 7
  • 43
  • 238
  • Sorry for late answer. I have tried this and it seems to work quite well so far. However, what I noticed is that I seem to need to add_custom_target to get the custom command to be picked up. Adding one custom target/custom command pair per file I want to configure/build will pick up any changes and rebuild the file if necessary. The problem though is that this is very slow, in case there are a few custom targets. Currently I struggle with how to deal with several OUTPUT in add_custom_command and DEPENDS in add_custom_target. Getting this "make[2]: *** No rule to make target". – patrik Mar 08 '23 at 12:10
  • @patrik "_I seem to need to add_custom_target to get the custom command to be picked up_" use custom command if the output is used as a source file of all targets that need it. Otherwise, use custom target. – starball Mar 08 '23 at 18:49
  • @patrik "_I struggle with how to deal with several OUTPUT in add_custom_command and DEPENDS in add_custom_target_" show me what you are doing. You can reach me [in chat](https://chat.stackoverflow.com/users/11107541) – starball Mar 08 '23 at 18:50