3rdPartyPackages.cmake 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745
  1. #
  2. # Copyright (c) Contributors to the Open 3D Engine Project.
  3. # For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. #
  5. # SPDX-License-Identifier: Apache-2.0 OR MIT
  6. #
  7. include_guard()
  8. include(${LY_ROOT_FOLDER}/cmake/LySet.cmake)
  9. # OVERVIEW:
  10. # this is the Open 3D Engine Package system.
  11. # It allows you to host a package on a server and download it as needed when a target
  12. # requests that specific package, or manually whenever you want to do so.
  13. # Most users will just call ly_associate_package(...) to associate a package with a target
  14. # and the system will automatically get that package when a target asks for it as a
  15. # dependency. If you want to manually activate a package, you can use ly_download_associated_package
  16. # to bring its find* scripts into scope (and download if needs be)
  17. # and you can also use ly_set_package_download_location(..) to change it to install
  18. # somewhere besides the default install location.
  19. # cache variables:
  20. # LY_PACKAGE_SERVER_URLS:
  21. # missing packages are downloaded from this list of URLS, first one to match wins.
  22. # Besides normal http and https URLs, you can also use FILE urls,
  23. # for example "file:///mnt/d/lyengine/packageSource/packages;file:///d:/lyengine/packageSource/packages"
  24. # also allowed:
  25. # "s3://bucketname" (it will use LYPackage_S3Downloader.cmake to download it from a s3 bucket)
  26. set(LY_PACKAGE_SERVER_URLS "https://d3t6xeg4fgfoum.cloudfront.net" CACHE STRING "Server URLS to fetch packages from")
  27. # Note: if you define the "LY_PACKAGE_SERVER_URLS" environment variable
  28. # it will be added to this value in the front, so that users can set
  29. # an env var and use that as an "additional" set of servers beyond the default set.
  30. if (DEFINED ENV{LY_PACKAGE_SERVER_URLS})
  31. set(LY_PACKAGE_SERVER_URLS $ENV{LY_PACKAGE_SERVER_URLS} ${LY_PACKAGE_SERVER_URLS})
  32. endif()
  33. # If you keep packages after downloading, then they can be moved to a network share
  34. # or checked into source control so that others on the same project can avoid re-downloading
  35. set(LY_PACKAGE_KEEP_AFTER_DOWNLOADING TRUE CACHE BOOL "If enabled, packages will be kept after downloading them for later re-use")
  36. set(LY_PACKAGE_DOWNLOAD_CACHE_LOCATION @LY_3RDPARTY_PATH@/downloaded_packages CACHE PATH "Download location for packages (Defaults to @LY_3RDPARTY_PATH@/downloaded_packages)")
  37. if (DEFINED ENV{LY_PACKAGE_DOWNLOAD_CACHE_LOCATION})
  38. set(LY_PACKAGE_DOWNLOAD_CACHE_LOCATION $ENV{LY_PACKAGE_DOWNLOAD_CACHE_LOCATION})
  39. endif()
  40. string(CONFIGURE ${LY_PACKAGE_DOWNLOAD_CACHE_LOCATION} LY_PACKAGE_DOWNLOAD_CACHE_LOCATION @ONLY)
  41. # LY_PACKAGE_UNPACK_LOCATION - you can change this to any path reachable.
  42. set(LY_PACKAGE_UNPACK_LOCATION @LY_3RDPARTY_PATH@/packages CACHE PATH "Unpack location of downloaded packages (Defaults to @LY_3RDPARTY_PATH@/packages)")
  43. if (DEFINED ENV{LY_PACKAGE_UNPACK_LOCATION})
  44. set(LY_PACKAGE_UNPACK_LOCATION $ENV{LY_PACKAGE_UNPACK_LOCATION})
  45. endif()
  46. string(CONFIGURE ${LY_PACKAGE_UNPACK_LOCATION} LY_PACKAGE_UNPACK_LOCATION @ONLY)
  47. # while developing you can set one or both to true to force auto downloads from your local cache
  48. set(LY_PACKAGE_VALIDATE_CONTENTS FALSE CACHE BOOL "If enabled, will fully validate every file in every package based on the SHA256SUMS file from the package")
  49. set(LY_PACKAGE_VALIDATE_PACKAGE FALSE CACHE BOOL "If enabled, will validate that the downloaded package files hash matches the expected hash even if already downloaded and verified before.")
  50. # you can also enable verbose/debug logging from the package system.
  51. set(LY_PACKAGE_DEBUG FALSE CACHE BOOL "If enabled, will output detailed information during package operations" )
  52. # ---- below this line, no cache variables or tweakables ---------
  53. ly_set(LY_PACKAGE_EXT ".tar.xz")
  54. ly_set(LY_PACKAGE_HASH_EXT ".tar.xz.SHA256SUMS")
  55. ly_set(LY_PACKAGE_CONTENT_HASH_EXT ".tar.xz.content.SHA256SUMS")
  56. set(LY_PACKAGE_DOWNLOAD_RETRY_COUNT 3 CACHE STRING "3")
  57. # accounts for it being undefined or blank
  58. if ("${LY_PACKAGE_DOWNLOAD_CACHE_LOCATION}" STREQUAL "")
  59. message(FATAL_ERROR "ly_package: LY_PACKAGE_DOWNLOAD_CACHE_LOCATION must be defined.")
  60. endif()
  61. # used to send messages and hide them unless the LY_PACKAGE_DEBUG var is true
  62. macro(ly_package_message)
  63. if (LY_PACKAGE_DEBUG)
  64. message(${ARGN})
  65. endif()
  66. endmacro()
  67. include(${LY_ROOT_FOLDER}/cmake/LYPackage_S3Downloader.cmake)
  68. # Attempts one time to download a file.
  69. # sets should_retry to true if the caller should retry due to an intermittent problem
  70. # Do not call this function, call download_file instead.
  71. function(download_file_internal)
  72. set(_oneValueArgs URL TARGET_FILE EXPECTED_HASH RESULTS SHOULD_RETRY)
  73. cmake_parse_arguments(download_file_internal "" "${_oneValueArgs}" "" ${ARGN})
  74. if(NOT download_file_internal_URL)
  75. message(FATAL_ERROR "no URL arg passed into download_file_internal, it is required.")
  76. endif()
  77. if(NOT download_file_internal_TARGET_FILE)
  78. message(FATAL_ERROR "no TARGET_FILE arg passed into download_file_internal, it is required.")
  79. endif()
  80. if(NOT download_file_internal_EXPECTED_HASH)
  81. message(FATAL_ERROR "no EXPECTED_HASH arg passed into download_file_internal, it is required.")
  82. endif()
  83. if(NOT download_file_internal_RESULTS)
  84. message(FATAL_ERROR "no RESULTS arg passed into download_file_internal, it is required.")
  85. endif()
  86. if(NOT download_file_internal_SHOULD_RETRY)
  87. message(FATAL_ERROR "no SHOULD_RETRY arg passed into download_file_internal, it is required.")
  88. endif()
  89. set(${download_file_internal_RESULTS} "-1;unknown_error" PARENT_SCOPE)
  90. unset(${download_file_internal_SHOULD_RETRY} PARENT_SCOPE)
  91. # note that below "results" is a local variable and download_file_internal_RESULTS will be set to it before exit.
  92. ly_is_s3_url(${download_file_internal_URL} result_is_s3_bucket)
  93. if (result_is_s3_bucket)
  94. ly_s3_download("${download_file_internal_URL}" ${download_file_internal_TARGET_FILE} results)
  95. else()
  96. file(DOWNLOAD "${download_file_internal_URL}" ${download_file_internal_TARGET_FILE} STATUS results TLS_VERIFY ON LOG logic)
  97. list(APPEND results ${log})
  98. endif()
  99. list(GET results 0 code_returned)
  100. # sometimes, the server returns a non-error code but still the file is zero bytes.
  101. # in this case, we need to return a failure. We won't ever use a 0 byte file.
  102. if (EXISTS ${download_file_internal_TARGET_FILE})
  103. file(SIZE ${download_file_internal_TARGET_FILE} target_size)
  104. if(target_size EQUAL 0)
  105. if(code_returned EQUAL 0)
  106. # the server said "OK" but gave us a bad file. Change this to not OK!
  107. ly_package_message("Server gave us a zero byte file but still said ${results}, retrying...")
  108. set(code_returned 22)
  109. set(results "22;\nThe requested URL returned error: 500 Internal Error - Zero byte file returned despite good exit code\n")
  110. set(${download_file_internal_SHOULD_RETRY} TRUE PARENT_SCOPE)
  111. endif()
  112. endif()
  113. else()
  114. # the file was not created. If the server still returned OK then we need to change this to not OK
  115. if (code_returned EQUAL 0)
  116. ly_package_message("Server gave us no file, but said ${results}, retrying...")
  117. set(code_returned 22)
  118. set(results "22;\nThe requested URL returned error: 500 Internal Error - Zero byte file returned despite good exit code\n")
  119. set(${download_file_internal_SHOULD_RETRY} TRUE PARENT_SCOPE)
  120. endif()
  121. endif()
  122. if(code_returned EQUAL 0)
  123. # code 0 means success, but we still need to hash the file.
  124. file(SHA256 ${download_file_internal_TARGET_FILE} hash_of_downloaded_file)
  125. if (NOT "${hash_of_downloaded_file}" STREQUAL "${download_file_internal_EXPECTED_HASH}" )
  126. set(results "1;Downloaded successfully, but the file hash did not match expected hash!")
  127. set(code_returned 1)
  128. endif()
  129. endif()
  130. if(code_returned)
  131. # non zero means it failed to download
  132. # however, cmake will leave the file open, and zero bytes.
  133. file(REMOVE ${download_file_internal_TARGET_FILE})
  134. # parse the error. If its 22, it means it was an HTTP error from curl.
  135. # we'll go to the bother of parsing the error and replacing it with the actual error code.
  136. # since the curl error is pointless.
  137. # note that the following code is similar to the code that's build into CMake
  138. # in its ExternalData.cmake file that ships.
  139. if (${code_returned} EQUAL 22)
  140. # extract the http response code if possible!
  141. string(REGEX MATCH "The requested URL returned error\\:([^\n]*)[\n]" found_string "${results}")
  142. if (found_string)
  143. # message the log before you replace it for debugging
  144. ly_package_message("${results}")
  145. # replace it.
  146. set(results ${code_returned} ${found_string})
  147. # if we get here, 'found_string' contains the one line response code from the server
  148. # which takes the form of ' response code - desciption of response code'
  149. # for example, ' 404 - Not Found'. It will only contain this one line.
  150. # See if its a 500 or 503 code specifically, if so, we need to retry.
  151. if (found_string MATCHES "500" OR found_string MATCHES "503")
  152. ly_package_message("500 or 503 code returned from server, will retry...")
  153. set(${download_file_internal_SHOULD_RETRY} TRUE PARENT_SCOPE)
  154. endif()
  155. endif()
  156. endif()
  157. endif()
  158. set(${download_file_internal_RESULTS} ${results} PARENT_SCOPE)
  159. endfunction()
  160. # Downloads a file with the ability to retry.
  161. # uses download_file_internal to actually download the file.
  162. # currently works on file://, s3://, ftp://, http://, https:// ... and whatever
  163. # else the file(DOWNLOAD ..) function supports.
  164. # if you add another downloader, see that it returns a list, where the first element
  165. # in the list is the error code, and then the rest of the list is the error(s), just like
  166. # the file(DOWNLOAD ... ) function does.
  167. # sets should_retry to a non null value if you should retry this download.
  168. function(download_file)
  169. set(_oneValueArgs URL TARGET_FILE EXPECTED_HASH RESULTS)
  170. cmake_parse_arguments(download_file "" "${_oneValueArgs}" "" ${ARGN})
  171. if(NOT download_file_URL)
  172. message(FATAL_ERROR "no URL arg passed into download_file, it is required.")
  173. endif()
  174. if(NOT download_file_TARGET_FILE)
  175. message(FATAL_ERROR "no TARGET_FILE arg passed into download_file, it is required.")
  176. endif()
  177. if(NOT download_file_EXPECTED_HASH)
  178. message(FATAL_ERROR "no EXPECTED_HASH arg passed into download_file, it is required.")
  179. endif()
  180. if(NOT download_file_RESULTS)
  181. message(FATAL_ERROR "no RESULTS arg passed into download_file, it is required.")
  182. endif()
  183. set(${download_file_RESULTS} "-1;unknown_error" PARENT_SCOPE)
  184. foreach(retry_count RANGE 0 ${LY_PACKAGE_DOWNLOAD_RETRY_COUNT})
  185. download_file_internal( URL ${download_file_URL} TARGET_FILE ${download_file_TARGET_FILE} RESULTS results EXPECTED_HASH ${download_file_EXPECTED_HASH} SHOULD_RETRY should_retry)
  186. if (NOT should_retry)
  187. break()
  188. endif()
  189. ly_package_message("${retry_count} / ${LY_PACKAGE_DOWNLOAD_RETRY_COUNT} download retry attempts.")
  190. endforeach()
  191. set(${download_file_RESULTS} ${results} PARENT_SCOPE)
  192. endfunction()
  193. #! ly_package_internal_download_package - note, the list of 3rd party urls is a list!
  194. # given a package name it will loop over the servers in the list and try each one
  195. # until one has the file AND has the correct hash for that file.
  196. function(ly_package_internal_download_package package_name url_variable)
  197. unset(${url_variable} PARENT_SCOPE)
  198. unset(error_messages)
  199. # to avoid spamming with useless repeated warnings, we save
  200. # a global that indicates we already failed to find it
  201. if (NOT LY_PACKAGE_SERVER_URLS)
  202. message(SEND_ERROR "ly_package: - LY_PACKAGE_SERVER_URLS is empty, cannot download packages. enable LY_PACKAGE_DEBUG for details")
  203. return()
  204. endif()
  205. ly_get_package_expected_hash(${package_name} package_expected_hash)
  206. foreach(server_url ${LY_PACKAGE_SERVER_URLS})
  207. set(download_url ${server_url}/${package_name}${LY_PACKAGE_EXT})
  208. ly_package_get_target_cache(${package_name} package_download_cache_location)
  209. set(download_target ${package_download_cache_location}/${package_name}${LY_PACKAGE_EXT})
  210. file(REMOVE ${download_target})
  211. ly_package_message(STATUS "ly_package: trying to download ${download_url} to ${download_target}")
  212. ly_get_package_expected_hash(${package_name} expected_package_hash)
  213. download_file(URL ${download_url} TARGET_FILE ${download_target} EXPECTED_HASH ${expected_package_hash} RESULTS results)
  214. list(GET results 0 status_code)
  215. if (${status_code} EQUAL 0 AND EXISTS ${download_target})
  216. set(${url_variable} ${server_url} PARENT_SCOPE)
  217. ly_package_message(STATUS "ly_package: - downloaded ${server_url} for package ${package_name}")
  218. return()
  219. else()
  220. # remove the status code and treat the rest of the list as the error.
  221. list(REMOVE_AT results 0)
  222. set(current_error_message "Error from server ${server_url} - ${status_code} - ${results}")
  223. #strip whitespace
  224. string(REGEX REPLACE "[ \t\r\n]$" "" current_error_message "${current_error_message}")
  225. list(APPEND error_messages "${current_error_message}")
  226. # we can't keep the file, sometimes it makes a zero-byte file!
  227. file(REMOVE ${download_target})
  228. endif()
  229. endforeach()
  230. # note that we FATAL_ERROR here because otherwise, some of the packages we provide would fall through
  231. # and use unknown versions possibly present somewhere in the user's system - but if we wanted that to happen
  232. # we wouldn't have used a ly-package-association in the first place! Continuing from there would just cause
  233. # a cascade of errors even harder to track down.
  234. set(final_error_message "ly_package: - Unable to get package ${package_name} from any download server. Enable LY_PACKAGE_DEBUG to debug.")
  235. foreach(error_message ${error_messages})
  236. set(final_error_message "${final_error_message}\n${error_message}")
  237. endforeach()
  238. message(FATAL_ERROR "${final_error_message}")
  239. endfunction()
  240. # parse_sha256sums_line
  241. # given a line of text that is in the SHA256SUMS format, digest it and output it
  242. # as a pair of variables of your choice.
  243. # will set output_hash to be the hex digest string
  244. # will set output_file_name to be the filename that must hash to that value
  245. # will unset them if an error occurs.
  246. function(parse_sha256sums_line input_hash_line output_hash output_file_name)
  247. # the official SHA256SUMS format is actually
  248. # the hash, followed by exactly one space character, followed
  249. # by either another space or an asterisk (indicating text or binary, space is text)
  250. # followed by the name of the file for the rest of the line (may contain more spaces)
  251. # the actual GNU implementations always binary hash, and so will we, so we'll ignore
  252. # the hash type character (text or binary)
  253. # this is why we capture three groups (hash, type, filename)
  254. # but only output 2 groups (1 and 3), hash and filename
  255. set(REGEX_EXPRESSION "^([A-Za-z0-9]*) ( |\\*)(.*)$")
  256. string(REGEX REPLACE "${REGEX_EXPRESSION}" "\\1;\\3" temp_list "${input_hash_line}")
  257. # hash list is now: expected_file_hash;file_name"
  258. list(LENGTH temp_list hash_length)
  259. if (NOT ${hash_length} EQUAL 2)
  260. unset(${output_hash} PARENT_SCOPE)
  261. unset(${output_file_name} PARENT_SCOPE)
  262. else()
  263. list(GET temp_list 0 temp_hash)
  264. list(GET temp_list 1 temp_filename)
  265. set(${output_hash} ${temp_hash} PARENT_SCOPE)
  266. set(${output_file_name} ${temp_filename} PARENT_SCOPE)
  267. endif()
  268. endfunction()
  269. # ly_validate_sha256sums_file -- internal function
  270. # given the path to a SHA256SUMS file and a working directory,
  271. # verifies the hashes based on the current settings.
  272. # --- sets HASH_WAS_VALID TRUE on parent scope if valid, FALSE otherwise.
  273. # Note that it does not currently check if extra files are present, only that
  274. # each file that is supposed to be there, is there and has the right hash.
  275. function(ly_validate_sha256sums_file working_directory path_to_sha256sums_file)
  276. set(HASH_WAS_VALID FALSE PARENT_SCOPE)
  277. if (NOT EXISTS ${path_to_sha256sums_file})
  278. ly_package_message(STATUS "ly_package: Could not find SHA256SUMS file: ${path_to_sha256sums_file}")
  279. return()
  280. endif()
  281. # lines in a SHA256SUMS file take the form of "hash *filename"
  282. # that is, the actual 256hash in hex decimals, whitespace, then an asterisk
  283. # and then the file name that must have that hash.
  284. set(ANY_HASH_MISMATCHES FALSE)
  285. # we only try to do any kind of hashing if the VALIDATE_CONTENTS flag is on
  286. # otherwise we just check for the presence of required files.
  287. # note that VALIDATE_CONTENTS is forced to true for any package when we download
  288. # it the first time. Its only set to true after that if the user forces it to be enabled.
  289. file(STRINGS ${path_to_sha256sums_file} hash_data ENCODING UTF-8)
  290. foreach(hash_line IN ITEMS ${hash_data})
  291. parse_sha256sums_line("${hash_line}" expected_file_hash file_name)
  292. if (NOT expected_file_hash OR NOT file_name)
  293. message(SEND_ERROR "ly_package: Invalid format SHA256SUMS file: ${path_to_sha256sums_file} line ${hash_line} - cannot verify hashes. Enable LY_PACKAGE_DEBUG to debug.")
  294. return()
  295. endif()
  296. if (EXISTS ${working_directory}/${file_name})
  297. if (LY_PACKAGE_VALIDATE_CONTENTS)
  298. file(SHA256 ${working_directory}/${file_name} existing_hash)
  299. if (NOT "${existing_hash}" STREQUAL "${expected_file_hash}" )
  300. ly_package_message(STATUS "ly_package: File hash mismatch: ${working_directory}/${file_name}")
  301. set(ANY_HASH_MISMATCHES TRUE)
  302. else()
  303. ly_package_message(STATUS "ly_package: File hash matches: ${expected_file_hash} - ${file_name}")
  304. endif()
  305. endif()
  306. else()
  307. ly_package_message(STATUS "ly_package: Expected file was not found: ${working_directory}/${file_name}")
  308. set(ANY_HASH_MISMATCHES TRUE)
  309. endif()
  310. endforeach()
  311. if (${ANY_HASH_MISMATCHES})
  312. ly_package_message(STATUS "ly_package: Validation failed - files were missing, or had hash mismatches.")
  313. else()
  314. set(HASH_WAS_VALID TRUE PARENT_SCOPE)
  315. endif()
  316. endfunction()
  317. function(ly_package_get_target_folder package_name output_variable_name)
  318. # is it grafted onto the tree elsewhere?
  319. get_property(overridden_location GLOBAL PROPERTY LY_PACKAGE_DOWNLOAD_LOCATION_${package_name})
  320. if (overridden_location)
  321. set(${output_variable_name} ${overridden_location} PARENT_SCOPE)
  322. elseif(NOT "${LY_PACKAGE_UNPACK_LOCATION}" STREQUAL "")
  323. set(${output_variable_name} ${LY_PACKAGE_UNPACK_LOCATION} PARENT_SCOPE)
  324. else()
  325. message(WARNING "ly_package: Could not locate the LY_PACKAGE_UNPACK_LOCATION variable"
  326. "'${LY_PACKAGE_UNPACK_LOCATION}' please fill it in!"
  327. " To compensate, this script will unpack into the build folder")
  328. set(${output_variable_name} ${CMAKE_BINARY_DIR} PARENT_SCOPE)
  329. endif()
  330. endfunction()
  331. #! Get the target cache folder
  332. function(ly_package_get_target_cache package_name output_variable_name)
  333. get_property(overridden_location GLOBAL PROPERTY LY_PACKAGE_DOWNLOAD_CACHE_LOCATION_${package_name})
  334. if (overridden_location)
  335. set(${output_variable_name} ${overridden_location} PARENT_SCOPE)
  336. else()
  337. set(${output_variable_name} ${LY_PACKAGE_DOWNLOAD_CACHE_LOCATION} PARENT_SCOPE)
  338. endif()
  339. endfunction()
  340. #! given the name of a package, validate that all files are present and match as appropriate
  341. function(ly_validate_package package_name)
  342. ly_package_message(STATUS "ly_package: Validating ${package_name}...")
  343. unset(${package_name}_VALIDATED PARENT_SCOPE)
  344. ly_package_get_target_folder(${package_name} DOWNLOAD_LOCATION)
  345. if (NOT EXISTS "${DOWNLOAD_LOCATION}/${package_name}")
  346. ly_package_message(STATUS "ly_package: - ${package_name} is missing from ${DOWNLOAD_LOCATION}")
  347. return()
  348. endif()
  349. set(hash_file_name ${DOWNLOAD_LOCATION}/${package_name}/SHA256SUMS)
  350. set(json_file_name ${DOWNLOAD_LOCATION}/${package_name}/PackageInfo.json)
  351. if (NOT EXISTS ${hash_file_name})
  352. ly_package_message(STATUS "Hash file missing from package ${package_name} (or package does not exist at all)")
  353. return()
  354. endif()
  355. if (NOT EXISTS ${json_file_name})
  356. ly_package_message(STATUS "Package info file missing from package: ${json_file_name}")
  357. return()
  358. endif()
  359. set(package_stamp_file_name ${DOWNLOAD_LOCATION}/${package_name}.stamp)
  360. if (NOT EXISTS ${package_stamp_file_name})
  361. # This can happen because of a previous version not making these stamp files in the correct place.
  362. # In order to avoid re-downloading the package we react to a missing stamp file by creating a new one.
  363. # This will cause any logic that wants to do things if the package is 'newer' to re-run, which is
  364. # safer than the alternative of not running things that do need to run when packages are downloaded.
  365. ly_package_message(STATUS "ly_package: Stamp file was missing, restoring: ${package_stamp_file_name}")
  366. file(TOUCH ${package_stamp_file_name})
  367. endif()
  368. if (LY_PACKAGE_VALIDATE_PACKAGE)
  369. # this message is unconditional because its not the default to do this and also its much slower.
  370. # The package hash is always checked automatically on first downloard regardless of the value of this
  371. # variable, so if this variable is true, a user explicitly asked to do this.
  372. message(STATUS "Checking downloaded package ${package_name} because LY_PACKAGE_VALIDATE_PACKAGE is TRUE")
  373. ly_package_get_target_cache(${package_name} package_download_cache_location)
  374. set(temp_download_target ${package_download_cache_location}/${package_name}${LY_PACKAGE_EXT})
  375. ly_get_package_expected_hash(${package_name} expected_package_hash)
  376. if (EXISTS ${temp_download_target})
  377. file(SHA256 ${temp_download_target} existing_hash)
  378. endif()
  379. if (NOT "${existing_hash}" STREQUAL "${expected_package_hash}" )
  380. # either the hash doesn't match or the file doesn't exist. Either way, we need to force download it again
  381. ly_package_message(STATUS "LY_PACKAGE_VALIDATE_PACKAGE : $[package_name}${LY_PACKAGE_EXT} is either missing or has the wrong hash, re-downloading")
  382. return()
  383. endif()
  384. endif()
  385. if (NOT LY_PACKAGE_VALIDATE_CONTENTS)
  386. ly_package_message(STATUS "Basic validation checks performed only becuase LY_PACKAGE_VALIDATE_CONTENTS is not enabled.")
  387. set(${package_name}_VALIDATED TRUE PARENT_SCOPE)
  388. ly_package_message(STATUS "ly_package: Validated ${package_name} - Basic Validation OK")
  389. return()
  390. endif()
  391. ly_validate_sha256sums_file(
  392. ${DOWNLOAD_LOCATION}/${package_name}
  393. ${DOWNLOAD_LOCATION}/${package_name}/SHA256SUMS)
  394. if (HASH_WAS_VALID)
  395. set(${package_name}_VALIDATED TRUE PARENT_SCOPE)
  396. ly_package_message(STATUS "ly_package: Validated ${package_name} - Full Validation OK")
  397. endif()
  398. endfunction()
  399. # ly_force_download_package
  400. # forces the download of a third party library regardless of current situation
  401. # package_name is like 'zlib-1.2.8-platform', not a file name or URL.
  402. # ---> Sets ${package_name}_VALIDATED on parent scope. TRUE only if the package
  403. # was successfully validated, including hash of contents.
  404. function(ly_force_download_package package_name)
  405. unset(${package_name}_FOUND PARENT_SCOPE)
  406. unset(${package_name}_VALIDATED PARENT_SCOPE)
  407. ly_package_get_target_folder(${package_name} DOWNLOAD_LOCATION)
  408. # this function contains a REMOVE_RECURSE. Because of that, we're going to do extra
  409. # validation on the inputs.
  410. # its not good enough for the variable to just exist but be empty, so we build strings
  411. if ("${package_name}" STREQUAL "" OR "${DOWNLOAD_LOCATION}" STREQUAL "")
  412. message(FATAL_ERROR "ly_package: ly_force_download_package called with invalid params! Enable LY_PACKAGE_DEBUG to debug.")
  413. endif()
  414. set(final_folder ${DOWNLOAD_LOCATION}/${package_name})
  415. # is the package already present in the download cache, with the correct hash?
  416. ly_package_get_target_cache(${package_name} package_download_cache_location)
  417. set(temp_download_target ${package_download_cache_location}/${package_name}${LY_PACKAGE_EXT})
  418. ly_get_package_expected_hash(${package_name} expected_package_hash)
  419. # can we reuse the download we already have in our download cache?
  420. if (EXISTS ${temp_download_target})
  421. ly_package_message(STATUS "The target ${temp_download_target} exists")
  422. file(SHA256 ${temp_download_target} existing_hash)
  423. endif()
  424. if (NOT "${existing_hash}" STREQUAL "${expected_package_hash}" )
  425. file(REMOVE ${temp_download_target})
  426. # we print this message unconditionally because downloading a package
  427. # can take time and we only get here if its missing in the first place, so
  428. # this should happen once on the very first configure
  429. message(STATUS "Downloading package into ${final_folder}")
  430. ly_package_message(STATUS "ly_package: - downloading package '${package_name}' to '${final_folder}'")
  431. ly_package_internal_download_package(${package_name} ${temp_download_target})
  432. # The above function will try every download location, with retries, so by the time we get here, the
  433. # operation is either done, or has completely failed.
  434. if (NOT EXISTS ${temp_download_target})
  435. # the system will have already issued errors, no need to issue more.
  436. return()
  437. endif()
  438. else()
  439. ly_package_message(STATUS "ly_package: - package already correct hash ${temp_download_target}, re-using")
  440. endif()
  441. if (EXISTS ${DOWNLOAD_LOCATION}/${package_name})
  442. ly_package_message(STATUS "ly_package: - removing folder ${DOWNLOAD_LOCATION}/${package_name} to replace it...")
  443. file(REMOVE_RECURSE ${DOWNLOAD_LOCATION}/${package_name})
  444. if (EXISTS ${DOWNLOAD_LOCATION}/${package_name})
  445. message(SEND_ERROR "ly_package: -folder ${DOWNLOAD_LOCATION}/${package_name} could not be removed. Check if some program has it open (VSCode, VS, Terminal windows, ...). Enable LY_PACKAGE_DEBUG to debug.")
  446. return()
  447. endif()
  448. endif()
  449. file(MAKE_DIRECTORY ${DOWNLOAD_LOCATION}/${package_name})
  450. ly_package_message(STATUS "ly_package: - unpacking package...")
  451. execute_process(COMMAND ${CMAKE_COMMAND} -E tar xf ${temp_download_target}
  452. WORKING_DIRECTORY ${final_folder} COMMAND_ECHO STDOUT OUTPUT_VARIABLE unpack_result)
  453. # For the runtime dependencies cases, we need the timestamps of the files coming from 3rdParty to be newer than the ones
  454. # from the output so the new versions get copied over. The untar from the previous step preserves timestamps so they
  455. # can produce binaries with older timestamps to the ones that are in the build output.
  456. file(GLOB_RECURSE package_files LIST_DIRECTORIES false ${final_folder}/*)
  457. file(TOUCH_NOCREATE ${package_files})
  458. if (NOT ${unpack_result} EQUAL 0)
  459. message(SEND_ERROR "ly_package: required package {package_name} could not be unpacked. Compile may fail! Enable LY_PACKAGE_DEBUG to debug.")
  460. return()
  461. else()
  462. if (NOT LY_PACKAGE_KEEP_AFTER_DOWNLOADING)
  463. ly_package_message(STATUS "ly_package: Removing package after unpacking (LY_PACKAGE_KEEP_AFTER_DOWNLOADING is ${LY_PACKAGE_KEEP_AFTER_DOWNLOADING})")
  464. file(REMOVE ${temp_download_target})
  465. endif()
  466. endif()
  467. # because we just downloaded this file, we are going to force full hashing validation.
  468. # future runs will use the setting or default, which is a quicker validation
  469. set(LY_PACKAGE_VALIDATE_CONTENTS_old ${LY_PACKAGE_VALIDATE_CONTENTS})
  470. set(LY_PACKAGE_VALIDATE_CONTENTS TRUE)
  471. ly_validate_package(${package_name})
  472. set(LY_PACKAGE_VALIDATE_CONTENTS ${LY_PACKAGE_VALIDATE_CONTENTS_old})
  473. set(${package_name}_VALIDATED ${package_name}_VALIDATED PARENT_SCOPE)
  474. set(package_stamp_file_name ${DOWNLOAD_LOCATION}/${package_name}.stamp)
  475. if (${package_name}_VALIDATED)
  476. # This we intentionally print out for each package that was actually downloaded from the internet, one time only:
  477. message(STATUS "Installed And Validated package at ${final_folder} - OK")
  478. # we also record a stamp file of when we did this, for use in other computations
  479. file(TOUCH ${package_stamp_file_name})
  480. else()
  481. file(REMOVE ${package_stamp_file_name})
  482. endif()
  483. endfunction()
  484. #! ly_enable_package: low-level function - adds a package to the auto package download system
  485. # Calling this will immediately make sure the package is present locally
  486. # and will add the local cache path to the additional module path (CMAKE_MODULE_PATH)
  487. # so that findxxxxx works
  488. # note that package_name here is the actual package name, not the association name.
  489. function(ly_enable_package package_name)
  490. # you can call this function as many times as you want, it will only try to validate the property once.
  491. # is it grafted onto the tree elsewhere?
  492. ly_package_get_target_folder(${package_name} DOWNLOAD_LOCATION)
  493. # add it to the prefixes so that we search here first
  494. # we add it in front so it can override any later paths, so "last one to declare" wins
  495. if (NOT "${DOWNLOAD_LOCATION}/${package_name}" IN_LIST "${CMAKE_MODULE_PATH}")
  496. set(CMAKE_MODULE_PATH ${DOWNLOAD_LOCATION}/${package_name} ${CMAKE_MODULE_PATH} PARENT_SCOPE)
  497. endif()
  498. get_property(existing_state GLOBAL PROPERTY LY_${package_name}_VALIDATED SET)
  499. if(NOT ${existing_state}) # note - check is for whether its SET, not whether its TRUE
  500. # if we get here, its not SET, so set it to FALSE pre-emptively so that
  501. # we don't try to download over and over, if the attempt to download fails.
  502. set_property(GLOBAL PROPERTY LY_${package_name}_VALIDATED FALSE)
  503. ly_validate_package(${package_name}) # sets VALIDATED in this scope.
  504. if (NOT ${package_name}_VALIDATED)
  505. # this will also validate it and set VALIDATED in this scope
  506. ly_force_download_package(${package_name})
  507. endif()
  508. if(${package_name}_VALIDATED)
  509. set_property(GLOBAL PROPERTY LY_${package_name}_VALIDATED TRUE)
  510. # this message is unconditional as it will help prove that the package even was
  511. # attempted to be mounted using our package system. In the absence of this message
  512. # its going to be difficult to know why a package is missing in the logs
  513. # its also consistent with cmake's other messages, like
  514. # 'using Windows Target SDK xxxxx' or 'using clang xyz'
  515. message(STATUS "Using package ${DOWNLOAD_LOCATION}/${package_name}")
  516. # if the package goes missing, we will reconfigure:
  517. # note that this is already likely the case, if you ever use find_package, but this can help when
  518. # the package contains nothing cmake-related, for example, its just extra tools or assets or
  519. # similar. Packages are required to have PackageInfo.json at the root at minimum.
  520. set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${DOWNLOAD_LOCATION}/${package_name}/PackageInfo.json)
  521. endif()
  522. endif()
  523. endfunction()
  524. #! ly_associate_package - Main public function
  525. # - allows you to associate an actual package name ('zlib-1.2.8-multiplatform')
  526. # with any number of targets that are expected to be inside the package.
  527. # Associating packages with targets will cause cmake to download the package (if necessary),
  528. # and ensure the path to the package root is added to the find_package search paths.
  529. # For example
  530. # ly_associate_package(TARGETS zlib PACKAGE_NAME zlib-1.2.8-multiplatform PACKAGE_HASH e6f34b8ac16acf881e3d666ef9fd0c1aee94c3f69283fb6524d35d6f858eebbb)
  531. # - this will cause it to automatically download and activate this package if it finds a target that
  532. # depends on '3rdParty::zlib' in its runtime or its build time dependency list.
  533. # - note that '3rdParty' is implied, do not specify it in the TARGETS list.
  534. function(ly_associate_package)
  535. set(_oneValueArgs PACKAGE_NAME PACKAGE_HASH)
  536. set(_multiValueArgs TARGETS)
  537. cmake_parse_arguments(ly_associate_package "" "${_oneValueArgs}" "${_multiValueArgs}" ${ARGN})
  538. if(NOT ly_associate_package_TARGETS)
  539. message(FATAL_ERROR "ly_associate_package was called without the TARGETS argument, at least one target is required")
  540. endif()
  541. if(NOT ly_associate_package_PACKAGE_NAME)
  542. message(FATAL_ERROR "ly_associate_package was called without the PACKAGE_NAME argument, this is required")
  543. endif()
  544. if(NOT ly_associate_package_PACKAGE_HASH)
  545. message(FATAL_ERROR "ly_associate_package was called without the PACKAGE_HASH argument, this is required")
  546. endif()
  547. foreach(find_package_name ${ly_associate_package_TARGETS})
  548. set_property(GLOBAL PROPERTY LY_PACKAGE_ASSOCIATION_${find_package_name} ${ly_associate_package_PACKAGE_NAME})
  549. set_property(GLOBAL PROPERTY LY_PACKAGE_HASH_${ly_associate_package_PACKAGE_NAME} ${ly_associate_package_PACKAGE_HASH})
  550. endforeach()
  551. set_property(GLOBAL APPEND PROPERTY LY_PACKAGE_NAMES ${ly_associate_package_PACKAGE_NAME})
  552. set_property(GLOBAL PROPERTY LY_PACKAGE_TARGETS_${ly_associate_package_PACKAGE_NAME} ${ly_associate_package_TARGETS})
  553. endfunction()
  554. #! Given a package find_package name (eg, 'zlib' not the actual package name)
  555. # will set output_variable to the package id iff the package has a package
  556. # association declared, otherwise will unset it.
  557. function(ly_get_package_association find_package_name output_variable)
  558. unset(${output_variable})
  559. get_property(is_associated GLOBAL PROPERTY LY_PACKAGE_ASSOCIATION_${find_package_name})
  560. if (is_associated)
  561. set(${output_variable} ${is_associated} PARENT_SCOPE)
  562. endif()
  563. endfunction()
  564. # given a package name (as in, the actual name of the package, not its associated find libraries)
  565. # return the expected download package hash.
  566. macro(ly_get_package_expected_hash actual_package_name output_variable)
  567. unset(${output_variable})
  568. get_property(package_hash_found GLOBAL PROPERTY LY_PACKAGE_HASH_${actual_package_name})
  569. if (package_hash_found)
  570. set(${output_variable} ${package_hash_found})
  571. else()
  572. # This is a fatal error because it is a programmer error and ignoring hashes
  573. # could be a security problem.
  574. message(FATAL_ERROR "ly_get_package_expected_hash could not find a hash for package ${actual_package_name}")
  575. endif()
  576. endmacro()
  577. # ly_set_package_download_location - OPTIONAL.
  578. # by default, packages are downloaded to the package root
  579. # at LY_PACKAGE_UNPACK_LOCATION - but if a package needs to be placed
  580. # elsewhere, use this.
  581. # note that package_name is expected to be the actual package name, not
  582. # the find_package(...) name!
  583. macro(ly_set_package_download_location package_name download_location)
  584. set_property(GLOBAL PROPERTY LY_PACKAGE_DOWNLOAD_LOCATION_${package_name} ${download_location})
  585. endmacro()
  586. # ly_set_package_download_cache_location - OPTIONAL.
  587. # Similar to ly_set_package_download_location, this set the download
  588. # cache location for a particular package.
  589. # note that package_name is expected to be the actual package name, not
  590. # the find_package(...) name!
  591. macro(ly_set_package_download_cache_location package_name download_location)
  592. set_property(GLOBAL PROPERTY LY_PACKAGE_DOWNLOAD_CACHE_LOCATION_${package_name} ${download_location})
  593. endmacro()
  594. # ly_download_associated_package - main public function
  595. # this just checks to see if the find_library_name (like 'zlib', not a package name)
  596. # is associated with a package, as above. If it is, it makes sure that the package
  597. # is brought into scope (and if necessary, downloaded.)
  598. macro(ly_download_associated_package find_library_name)
  599. unset(package_name)
  600. ly_get_package_association(${find_library_name} package_name)
  601. if (package_name)
  602. # it is an associated package.
  603. ly_enable_package(${package_name})
  604. endif()
  605. endmacro()
  606. # ly_package_is_newer_than(package_name reference output_variable)
  607. # will set output_variable to TRUE if and only if the package was downloaded
  608. # more recently than the reference file's timestamp.
  609. function(ly_package_is_newer_than package_name reference_file output_variable)
  610. unset(${output_variable} PARENT_SCOPE)
  611. ly_package_get_target_folder(${package_name} DOWNLOAD_LOCATION)
  612. set(package_stamp_file_name ${DOWNLOAD_LOCATION}/${package_name}.stamp)
  613. if (EXISTS ${package_stamp_file_name} AND ${package_stamp_file_name} IS_NEWER_THAN ${reference_file})
  614. set(${output_variable} TRUE PARENT_SCOPE)
  615. endif()
  616. endfunction()
  617. # if we're in script mode, we dont want to declare package associations
  618. if (NOT CMAKE_SCRIPT_MODE_FILE)
  619. # include the built in 3rd party packages that are for every platform.
  620. # you can put your package associations anywhere, but this provides
  621. # a good starting point.
  622. include(${LY_ROOT_FOLDER}/cmake/3rdParty/BuiltInPackages.cmake)
  623. endif()
  624. if(PAL_TRAIT_BUILD_HOST_TOOLS)
  625. include(${LY_ROOT_FOLDER}/cmake/LYWrappers.cmake)
  626. # Importing this globally to handle AUTOMOC, AUTOUIC, AUTORCC
  627. ly_parse_third_party_dependencies(3rdParty::Qt)
  628. endif()