1
0
mirror of https://github.com/Rogiel/CMakeDependency synced 2025-12-05 21:22:46 +00:00

Initial commit

This commit is contained in:
2019-10-28 12:47:01 -03:00
commit 15576b74ea
4 changed files with 487 additions and 0 deletions

61
CMakeDependency.cmake Normal file
View File

@@ -0,0 +1,61 @@
# CMake dependency: this is a little and lightweight helper tool that
# automatically downloads, extracts and configures dependencies based on a
# declaration on a .json file.
#
# To manually add a dependency, call `import_dependency(<name> URL <url>). After
# this call, CMake will have downloaded and extracted the dependency source. The
# extracted files are located at `${<name>_SOURCE_DIR}`.
#
# Also, it is possible to import a dependency JSON file by calling
# `import_dependencies_from_json(<json file>)`. All dependencies are
# automatically configured as declared in the JSON file. CMake will also
# regenerate any configuration needed if the JSON is ever changed.
set(CMDEP_GENERATOR_SCRIPT ${CMAKE_CURRENT_LIST_DIR}/DependencyGenerator.py)
set(CMDEP_DEPENDENCY_DECL_TMPL ${CMAKE_CURRENT_LIST_DIR}/DependencyDeclaration.cmake.in)
set(CMDEP_ROOT_DIR
${CMAKE_CURRENT_SOURCE_DIR}
CACHE PATH "A path that points to the location in which CMake dependency files should be downloaded and extracted to.")
set(CMDEP_ZIP_DIR
${CMDEP_ROOT_DIR}/zips
CACHE PATH "A path that points to the location in which ZIP files should be downloaded to.")
function(import_dependency name)
set(options)
set(oneValueArgs URL DOWNLOAD_NAME)
set(multiValueArgs)
cmake_parse_arguments(DEPENDENCY "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} )
cmake_parse_arguments(${name} "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} )
set(DEPENDENCY_NAME ${name})
configure_file(${CMDEP_DEPENDENCY_DECL_TMPL} ${CMAKE_CURRENT_BINARY_DIR}/${name}.dep.cmake @ONLY)
include(${CMAKE_CURRENT_BINARY_DIR}/${name}.dep.cmake)
endfunction()
function(import_dependencies_from_json json)
set(json_dep_file ${json})
set(cmake_dep_file ${CMAKE_CURRENT_SOURCE_DIR}/CMakeLists.deps.txt)
find_package(Python3 COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND)
list(APPEND CMAKE_CONFIGURE_DEPENDS ${json_dep_file})
file(LOCK ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY GUARD FILE)
if(${json_dep_file} IS_NEWER_THAN ${cmake_dep_file} OR
${CMDEP_GENERATOR_SCRIPT} IS_NEWER_THAN ${cmake_dep_file})
execute_process(
COMMAND ${Python3_EXECUTABLE} ${CMDEP_GENERATOR_SCRIPT}
INPUT_FILE ${json_dep_file}
OUTPUT_FILE ${cmake_dep_file})
endif()
file(LOCK ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY RELEASE)
else()
message(STATUS "Python interpreter not found. CMake dependencies will NOT update automatically.")
endif()
include(${cmake_dep_file})
endfunction()
list(APPEND CMAKE_CONFIGURE_DEPENDS ${CMDEP_GENERATOR_SCRIPT})

View File

@@ -0,0 +1,94 @@
set(@DEPENDENCY_NAME@_URL
"@DEPENDENCY_URL@"
CACHE STRING "A path that points to a @DEPENDENCY_NAME@ URL to be downloaded. CMake will use this URL to download the file if needed.")
set(@DEPENDENCY_NAME@_DIR
${CMDEP_ROOT_DIR}/@DEPENDENCY_NAME@
CACHE PATH "A path that points to a @DEPENDENCY_NAME@ directory containing it's sources.")
set(@DEPENDENCY_NAME@_LOCK ${CMDEP_ROOT_DIR})
set(@DEPENDENCY_NAME@_SOURCE_DIR
${@DEPENDENCY_NAME@_DIR}
CACHE PATH "A path that points to a @DEPENDENCY_NAME@ source directory. If manually set, no downloading or extraction will take place and this will be used instead.")
set(@DEPENDENCY_NAME@_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/@DEPENDENCY_NAME@)
get_filename_component(@DEPENDENCY_NAME@_DOWNLOAD_NAME "${@DEPENDENCY_NAME@_URL}" NAME CACHE)
set(@DEPENDENCY_NAME@_ZIP
${CMDEP_ZIP_DIR}/${@DEPENDENCY_NAME@_DOWNLOAD_NAME}
CACHE PATH "A path that points to a @DEPENDENCY_NAME@ zip file. If this file does not exists, it will be downloaded.")
# To prevent other concurrent CMake instances from trying to download or extract
# the file, we lock the source directory. This will guarantee that only a single
# CMake instance downloads or extracts a file.
#
file(LOCK ${@DEPENDENCY_NAME@_LOCK} DIRECTORY GUARD FILE)
if(NOT EXISTS ${@DEPENDENCY_NAME@_SOURCE_DIR})
if(NOT EXISTS ${@DEPENDENCY_NAME@_ZIP})
get_filename_component(@DEPENDENCY_NAME@_ZIP_DIRECTORY "${@DEPENDENCY_NAME@_ZIP}" DIRECTORY)
if(NOT EXISTS ${@DEPENDENCY_NAME@_ZIP_DIRECTORY})
file(MAKE_DIRECTORY ${@DEPENDENCY_NAME@_ZIP_DIRECTORY})
endif()
message(STATUS "@DEPENDENCY_NAME@: Downloading from ${@DEPENDENCY_NAME@_URL}")
file(DOWNLOAD ${@DEPENDENCY_NAME@_URL}
${@DEPENDENCY_NAME@_ZIP}
SHOW_PROGRESS
# no TIMEOUT
STATUS status
LOG log)
list(GET status 0 status_code)
list(GET status 1 status_string)
if(status_code EQUAL 0)
message(STATUS "@DEPENDENCY_NAME@: Download complete")
else()
message(FATAL_ERROR "@DEPENDENCY_NAME@: error: downloading from '${@DEPENDENCY_NAME@_URL}' failed with error code ${status_code} (${status_string}) -- ${log}")
endif()
endif()
# Prepare a space for extracting:
#
set(tmp_dir "${CMAKE_CURRENT_BINARY_DIR}/@DEPENDENCY_NAME@-tmp")
file(MAKE_DIRECTORY "${tmp_dir}")
# Extract it:
#
message(STATUS "@DEPENDENCY_NAME@: extracting...")
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xfz ${@DEPENDENCY_NAME@_ZIP}
WORKING_DIRECTORY ${tmp_dir}
RESULT_VARIABLE rv)
if(NOT rv EQUAL 0)
message(STATUS "@DEPENDENCY_NAME@: extracting... [error clean up]")
file(REMOVE_RECURSE "${tmp_dir}")
message(FATAL_ERROR "@DEPENDENCY_NAME@: error: extract of '${@DEPENDENCY_NAME@_DOWNLOAD_NAME}' failed")
endif()
# Analyze what came out of the tar file:
#
message(STATUS "@DEPENDENCY_NAME@: extracting... [analysis]")
file(GLOB contents "${tmp_dir}/*")
list(REMOVE_ITEM contents "${tmp_dir}/.DS_Store")
list(LENGTH contents n)
if(NOT n EQUAL 1 OR NOT IS_DIRECTORY "${contents}")
set(contents "${tmp_dir}")
endif()
# Move "the one" directory to the final directory:
#
message(STATUS "@DEPENDENCY_NAME@: extracting... [rename]")
file(REMOVE_RECURSE ${@DEPENDENCY_NAME@_DIR})
get_filename_component(contents ${contents} ABSOLUTE)
file(RENAME ${contents} ${@DEPENDENCY_NAME@_DIR})
# Clean up:
#
message(STATUS "@DEPENDENCY_NAME@: extracting... [clean up]")
file(REMOVE_RECURSE "${tmp_dir}")
endif()
# We are now done. We can unlock the CMake directory and proceed.
#
file(LOCK ${@DEPENDENCY_NAME@_LOCK} DIRECTORY RELEASE)

115
DependencyGenerator.py Normal file
View File

@@ -0,0 +1,115 @@
import json, os, sys
from urllib.parse import urlparse
tab = " "*4
nl_indent = "\n"+tab*2
def escape(s, n):
return ' ' * n + s
def generate_dependency_decl(name, keys):
max_key_length = 0
for (key, value) in keys.items():
max_key_length = max(max_key_length, len(key))
s = "import_dependency(" + name + nl_indent + (nl_indent).join(
map(lambda k: k[0] + " " + escape(k[1], max_key_length - len(k[0]) + 3), keys.items())) + ")"
return s
j = json.load(sys.stdin)
for (name, dep_info) in j.items():
sys.stderr.write('Updating dependency definitions for ' + name + '\n')
target_info = dep_info["target"]
target_type = target_info["type"]
src_dir = "${" + name + "_SOURCE_DIR}/"
dep_args = {}
if "git" in dep_info:
git_info = dep_info["git"]
git_repo = git_info["repository"]
git_tag = git_info["tag"]
git_url_parsed = urlparse(git_repo)
if git_url_parsed.hostname == 'github.com':
parts = git_url_parsed.path.split("/")
user = parts[1]
repo = parts[2]
repo = repo[:-4] if repo.endswith('.git') else repo
dep_args["URL"] = "https://github.com/" + user + "/" + repo + "/archive/" + git_tag + ".zip"
dep_args["DOWNLOAD_NAME"] = repo + "-" + git_tag + ".zip"
else:
print('git is only supported for github repositories at the moment.', file=sys.stderr)
exit(-1)
elif "url" in dep_info:
dep_args["URL"] = " ".join(dep_info["url"])
if 'download_name' in dep_info:
dep_args["DOWNLOAD_NAME"] = dep_info['download_name']
print("# -- Dependency: " + name)
print(generate_dependency_decl(name, dep_args))
public_decl_type = "PUBLIC"
if target_type == "static":
if isinstance(target_info["srcs"], list):
srcs = (nl_indent).join(map(lambda x: src_dir + x,target_info["srcs"]))
else:
print("file(GLOB " + name + "_SRCS " + src_dir + target_info["srcs"] + ")")
srcs = "${" + name + "_SRCS}"
print("add_library("+name+" STATIC "+nl_indent+srcs+")")
if target_type == "interface":
print("add_library("+name+" INTERFACE)")
public_decl_type = "INTERFACE"
if target_type == "subdirectory":
if "cache" in target_info:
for (var_name, value) in target_info["cache"].items():
if isinstance(value, bool):
print("set("+var_name+" "+("ON" if value else "OFF")+" CACHE INTERNAL \"\" FORCE)")
print("add_subdirectory(${" + name + "_SOURCE_DIR} ${" + name + "_BINARY_DIR})")
if target_type == "cmake":
print("include(${PROJECT_SOURCE_DIR}/" + target_info["file"]+")\n")
continue
includes = []
if "public_includes" in target_info:
includes += list(map(lambda x: public_decl_type + " " + src_dir + x, target_info["public_includes"]))
if "private_includes" in target_info:
includes += list(map(lambda x: "PRIVATE " + src_dir + x, target_info["private_includes"]))
if len(includes) != 0:
print("target_include_directories("+name+" "+nl_indent+
nl_indent.join(includes)
+")")
defines = []
if "public_defines" in target_info:
defines += list(map(lambda x: public_decl_type + " " + x, target_info["public_defines"]))
if "private_defines" in target_info:
defines += list(map(lambda x: "PRIVATE " + x, target_info["private_defines"]))
if len(defines) != 0:
print("target_compile_definitions("+name+" "+nl_indent+
nl_indent.join(defines)
+")")
if "links" in target_info:
print("target_link_libraries("+name+" "+nl_indent+
nl_indent.join(map(lambda x: public_decl_type + ' ' + x, target_info["links"]))
+")")
if "aliases" in target_info:
for (tgt, src) in target_info["aliases"].items():
print("add_library("+tgt+" ALIAS "+src+")")
if "extra_cmake" in target_info:
print()
print("include(${PROJECT_SOURCE_DIR}/" + target_info["extra_cmake"]+")")
print()

217
README.md Normal file
View File

@@ -0,0 +1,217 @@
# CMakeDependency
A little and lightweight CMake helper tool that automatically downloads, extracts and configures dependencies based on a declaration on a .json file.
## Getting started
First you need to declare a dependency in your `dependencies.json` file:
```json
{
"JSON": {
"url": [
"https://github.com/nlohmann/json/releases/download/v3.7.0/include.zip"
],
"download_name": "json-3.7.0.zip",
"target": {
"type": "interface",
"public_includes": [
"."
]
}
}
}
```
In your CMakeLists.txt all you need is to include the module and import all dependencies from your JSON file:
```cmake
# Include the CMakeDependency module
include(CMakeDependency/CMakeDependency.cmake)
# Import dependencies from JSON
import_dependencies_from_json(${PROJECT_SOURCE_DIR}/dependencies.json)
```
You are done! `JSON` is now available as a target in CMake!
```cmake
target_link_libraries(MyTarget PUBLIC JSON)
```
Note that CMakeDependency will create a file called `CMakeLists.deps.txt` in `CMAKE_CURRENT_SOURCE_DIR`. This file contains the preprocessed JSON file that is included by CMake. You can commit this file into your repository if you don't want to depend on Python 3 when compiling.
## Dependencies
This CMake module requires **CMake 3.15** and up. **Python3** is required if you want to automatically generate the CMake dependencies from the JSON file.
## Declaring dependencies manually in CMake
You don't have to use the JSON file if you don't want to. You can import dependencies directly in CMake by calling `import_dependency`:
```cmake
import_dependency(JSON
URL https://github.com/nlohmann/json/releases/download/v3.7.0/include.zip
DOWNLOAD_NAME json-3.7.0.zip)
```
Note that `DOWNLOAD_NAME` is optional. If not given, CMakeDependency will resolve a name based on the given `URL`.
In this mode, no target is created but the ZIP file is downloaded and extracted. The extracted contents are in `JSON_SOURCE_DIR`. If this library is a CMake library, you can simply call `add_subdirectory` and be done with it.
```cmake
add_subdirectory(${JSON_SOURCE_DIR} ${JSON_BINARY_DIR})
```
or if the dependency is **NOT** a CMake project, you can manually create a target:
```cmake
add_library(JSON INTERFACE)
target_include_directories(JSON INTERFACE ${JSON_SOURCE_DIR})
```
## Overriding a dependency
If you don't want to duplicate your dependencies across multiple projects you can override the dependency source dir by setting `JSON_DIR` cache variable. You can do so when invoking CMake:
```shell script
cmake -DJSON_DIR="/my/json/lib" [...]
```
or from within CMake itself, before calling `import_dependency` or `import_dependencies_from_json`:
```cmake
set(JSON_DIR "/my/json/lib" CACHE PATH "" FORCE)
```
Node that when setting `JSON_DIR`, if the dependency is not downloaded it will be downloaded and imported to the given directory. If this is not something you want, for example, you want to use a system provided dependency, you can override the directory by setting `JSON_SOURCE_DIR`.
Additionally, you can override just the zip file by setting `JSON_ZIP`.
## `dependencies.json` by example
### Boost
This example downloads the Boost source zip and configures it by building the `system` library.
```json
{
"Boost": {
"url": [
"https://dl.bintray.com/boostorg/release/1.71.0/source/boost_1_71_0.zip"
],
"target": {
"type": "static",
"srcs": [
"libs/system/src/error_code.cpp"
],
"public_includes": [
"."
],
"public_defines": {
"BOOST_ALL_NO_LIB": "1",
"BOOST_ASIO_NO_TYPEID": "1"
},
"links": [
"$<$<PLATFORM_ID:Windows>:bcrypt.dll>"
]
}
}
}
```
### [Naios/continuable](https://github.com/Naios/continuable)
This will [Naios/continuable](https://github.com/Naios/continuable) and it's dependency [Naios/function2](https://github.com/Naios/function2) create both targets as a CMake interface library and link `Function2` into `Continuable`. As a consumer, you can simply link against `Continuabe` and CMake will add `Function2` transitively fot you.
```json
{
"Continuable": {
"git": {
"repository": "https://github.com/Naios/continuable.git",
"tag": "cacb84371a5e486567b4aed244ce742ea6509d7d"
},
"target": {
"type": "interface",
"public_includes": [
"include"
],
"links": [
"Function2"
]
}
},
"Function2": {
"git": {
"repository": "https://github.com/Naios/function2.git",
"tag": "7cd95374b0f1c941892bfc40f0ebb6564d33fdb9"
},
"target": {
"type": "interface",
"public_includes": [
"include"
]
}
}
}
```
### [Rogiel/PacketBuffer](https://github.com/Rogiel/PacketBuffer)
This will download [Rogiel/PacketBuffer](https://github.com/Rogiel/PacketBuffer) and import it by using CMake's `add_subdirectory` command.
```json
{
"PacketBuffer": {
"git": {
"repository": "https://github.com/Rogiel/PacketBuffer.git",
"tag": "37e76659eaf4ed7d4407d1ccc5e31dee5454ef2c"
},
"target": {
"type": "subdirectory"
}
}
}
```
### Poco
This example will download the Poco library and include `Poco.cmake` for further configuration. You can use `Poco_SOURCE_DIR` and `Poco_BINARY_DIR` if needed to configure your target.
This allows very flexible build configuration for dependencies.
```json
{
"Poco": {
"url": [
"https://pocoproject.org/releases/poco-1.9.4/poco-1.9.4.zip"
],
"target": {
"type": "cmake",
"file": "Poco.cmake"
}
}
}
```
## Advanced features
### Global cache variables
- **`CMDEP_ROOT_DIR`**: A path that points to the location in which CMake dependency files should be downloaded and extracted to. Defaults to `${CMAKE_CURRENT_SOURCE_DIR}`.
- **`CMDEP_ZIP_DIR`**: A path that points to the location in which ZIP files should be downloaded to. Defaults to `${CMDEP_ROOT_DIR}/zips`.
### Dependency cache variables
- **`<name>_URL`**: A path that points to a URL to be downloaded. CMake will use this URL to download the file if needed. Defaults to the value given in `import_dependency`.
- **`<name>_DIR`**: A path that points to a directory containing the dependency sources. Defaults to `${CMDEP_ROOT_DIR}/<name>`.
- **`<name>_SOURCE_DIR`**: A path that points to a source directory. If manually set, no downloading or extraction will take place and this will be used instead. Defaults to `${<name>_DIR}`.
- **`<name>_ZIP`**: A path that points to a zip file. If this file does not exists, it will be downloaded. You can use this to override the download and point to an already existing ZIP file. Defaults to `${CMDEP_ZIP_DIR}/<file name>.zip`.
## Concurrency with CMake
Some IDEs, such as CLion, use multiple CMake invocations for each configuration. This could cause problems when downloading dependencies to a common location. To avoid such problems, `CMakeDependency` uses CMake's advisory locking mechanism to prevent multiple CMake instances from downloading the same file.
This lock is acquired in the `CMDEP_ROOT_DIR` directory. Beware that CMake will create a `cmake.lock` file in this directory and you should exclude it from your source control if needed.
## TODO
- Improve documentation on `dependencies.json` syntax.