How to specify a target to be built only once

As part of my larger project, I need to retrieve a file from an external SVN repo to be included in the final RPM build step. This repo is kind of slow… about 50% of the time the download times out, but it’s annoying when you are in a heavy edit/compile cycle to have to wait on a useless download.

For now, I have a separate CMakeLists.txt file for this download. I wasn’t sure how to make it a target, so I ended up with this:

file(DOWNLOAD
  "http://${download_svnhost}/${download_source}"
  ${download_target}
  ${download_credentials}
  ${download_hash}
  INACTIVITY_TIMEOUT 3
  TIMEOUT            5
  STATUS             download_status
)

message(INFO " download status: ${download_status}")

### Add the documentation file(s) to the distribution
install(FILES
  ${download_target}
  COMPONENT   doc
  DESTINATION doc
  )

What I’d like to do is only download this file as a final step prior to building the packages. Can I get there from here?

You could download the file as part of an install(SCRIPT) or install(CODE) snippet. I would then skip the download if the file already exists and has a valid checksum.

Thanks. It took a bit of finagling; the documentation for install(SCRIPT|CODE) is a bit sparse, but I finally got this to more-or-less work

### Add the documentation file(s) to the distribution
install(CODE "message(INFO \" downloading user's manual (${download_target}) from ${download_url}\")"
  COMPONENT   doc
)
install(CODE
  "file(DOWNLOAD
     ${download_url}
     ${download_target}
     ${download_credentials}
     ${download_hash}
     STATUS download_status
  )"
  COMPONENT   doc
)
install(CODE "message(INFO \" download complete; status=${download_status}\")"
  COMPONENT   doc
)

install(FILES
  ${download_target}
  COMPONENT   doc
  DESTINATION doc
)

What wasn’t clear to me was that I still needed to have install(FILES) after the download (CODE apparently doesn’t honor the DESTINATION tag). It also wasn’t clear that I couldn’t have multiple commands in the CODE block, but I did find that multiple install(CODE) blocks worked.

What I stumbled upon digging through the generated cmake_install.cmake file is that every install command should have a COMPONENT tag (if you use components), otherwise it is attached to the “Unspecified” component. This has the undesirable side-effect of creating a “myprog-Unspecified-vers.arch.rpm” when building RPMs.

The piece I don’t have right is detecting an error in the download. I purposely put in a bogus host and the build completes without incident. Instead of raising an error, is creates a 0-length file and puts it in the RPM. Note that download_status is local to the install(CODE) command that downloads the file, and so is not viisble in the final message.

You should be able to do a message(FATAL_ERROR) to stop the install process if you detect a problem with the downloaded file.

can you suggest a snippet? As noted, I was unsuccessful in having multiple commands, and the status variable for the download is not visible outside that command.

Something like:

file(DOWNLOAD … STATUS download_res)
if (download_res)
  message(FATAL_ERROR "Failed to download")
endif ()

I had trouble with multiple commands inside a CODE so I tried SCRIPT. The problem I ran into was that SCRIPT didn’t inherit variables from the calling context. Or it may have been an order of parsing issue … see below.

After quite a bit of experimentation, I did figure out how to have multiple commands inside a CODE argument. Apparently CMake relies on newlines as statement delimiters, but newlines within strings are allowed. And I also learned the importance of understanding how strings are parsed. One of the key things I learned was that any "${variable}" strings have the variable substituted at generation time, not at run time. In order to reference variables set inside my script, I had to escape the $ to defer substitution until run time.

What I ended up with was this:

install(CODE "message(INFO \" url=${download_url}\n    target=${download_target}\")
file(DOWNLOAD
  ${download_url}
  ${download_target}
  ${download_credentials}
  ${download_hash}
  TIMEOUT 5
  LOG     my_log
  STATUS  download_status
)
list(GET download_status 0 dl_success)
if (\${dl_success} EQUAL 0)
  message(INFO \" downloaded; status=\${download_status}\")
else()
  message(FATAL_ERROR \" download failed; status=\${download_status}\n---\n\${my_log}---\")
endif()"
  COMPONENT   doc
)


install(FILES
  ${download_target}
  COMPONENT   doc
  DESTINATION doc
)

Once I read @ben.boeckel reply more closely and used the proper argument to message(), the packaging failed as desired.

Whew. Thanks for the help.