diff --git a/README.md b/README.md index a63597a..232c816 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,17 @@ The script can be executed in one of two modes: develop (default) and release. Dependencies used are specified in the `dependencies.json` files, although this file can be overridden at the command line. +The build system generated by the `update_repositories.py` script requires that the target repository has previously added [shacl::cmake](https://github.com/shacl/cmake) as a repository. + ## Dependency specifications Each supported component's list of dependencies is included in the `dependencies.json` file. Additional components can be added to this list or a user can override the file at the command line. The JSON file requires an entry with the same name as the component, as specified on the command line or inferred from the path. That entry should have a list of dictionaries, each representing a dependency. -Dependencies can be a string, which is taken as the name of the repository, assumed to be on the NJOY project on GitHub and using the default branch. Otherwise, dependencies can be a dictionary, where more options can be specified. +Dependencies can be a string, which is taken as the name of the repository, assumed to be on the NJOY project relative to where the project is hosted and using the default branch. Otherwise, dependencies can be a dictionary, where more options can be specified. -Dependencies specified as a dictionary must include `"name"` or `"remote"`. If `"name"` is included without `"remote"`, the remote is assumed to be `https://github.com/njoy/{name}`. If only `"remote"` is specified, `"name"` is assumed to be the basename of the path. Both can be included, which is necessary when the basename of the remote does not match the name, e.g. dimwits/DimensionalAnalysis. +Dependencies specified as a dictionary must include `"name"` or `"remote"`. If `"name"` is included without `"remote"`, the remote is assumed to be `../../njoy/{name}`. The relative URL capability is provided via [shacl::cmake](https://github.com/shacl/cmake). If only `"remote"` is specified, `"name"` is assumed to be the basename of the path. Both can be included, which is necessary when the basename of the remote does not match the name, e.g. dimwits/DimensionalAnalysis. Including `"tag"` or `"branch"` is optional, and if neither is provided, it defaults to the master branch. If both are provided, an error occurs. This file should include primarily live-at-head dependencies, so specifying a branch is typical. However, perhaps in the case of a third-party dependency or in an overridden dependency file, a specific Git commit hash or Git tag can be used instead. diff --git a/dependencies.json b/dependencies.json index f0351e1..8406849 100644 --- a/dependencies.json +++ b/dependencies.json @@ -1,24 +1,24 @@ { "ENDFtk": [ - "Log", + "njoy::Log", "catch-adapter", - "disco", + "njoy::disco", "hana-adapter", - "header-utilities", - {"name": "range-v3", - "remote": "https://github.com/ericniebler/range-v3", + "njoy::header-utilities", + {"name": "range-v3::range-v3", + "remote": "../../ericniebler/range-v3", "tag": "0.11.0"} ], "ACEtk": [ - "Log", + "njoy::Log", "catch-adapter", - {"name": "dimwits", "remote": "https://github.com/njoy/DimensionalAnalysis"} , - "disco", - "interpolation", + {"name": "njoy::dimwits", "remote": "../../njoy/DimensionalAnalysis"} , + "njoy::disco", + "njoy::interpolation", "hana-adapter", - "header-utilities", - {"name": "range-v3", - "remote": "https://github.com/ericniebler/range-v3", + "njoy::header-utilities", + {"name": "range-v3::range-v3", + "remote": "../../ericniebler/range-v3", "tag": "0.11.0"} ], "thermr": [ @@ -33,23 +33,24 @@ ], "Log": [ "catch-adapter", - {"remote": "https://github.com/gabime/spdlog", + {"name": "spdlog", + "remote": "../../gabime/spdlog", "tag": "a51b4856377a71f81b6d74b9af459305c4c644f8", "setup": "set( SPDLOG_BUILD_TESTING CACHE BOOL OFF )"} ], "GNDStk": [ "catch-adapter", - "Log", + "njoy::Log", "pugixml-adapter", {"name": "nlohmann_json", - "remote": "https://github.com/nlohmann/json", + "remote": "../../nlohmann/json", "tag": "v3.7.3", "setup": "set(JSON_BuildTests OFF CACHE INTERNAL \"\")"} ], "NJOY2016": false, "RECONR": [ {"name": "nlohmann_json", - "remote": "https://github.com/nlohmann/json", + "remote": "../../nlohmann/json", "tag": "v3.7.3", "setup": "set(JSON_BuildTests OFF CACHE INTERNAL \"\")"}, {"name": "eigen", @@ -60,17 +61,17 @@ "catch-adapter", "constants", "resonanceReconstruction", - "Log", + "njoy::Log", "twig", - "interpolation", - "elementary" + "njoy::interpolation", + "njoy::elementary" ], "catch-adapter": false, "constants": [ "range-v3-adapter", "catch-adapter", - "Log", - {"name": "dimwits", "remote": "https://github.com/njoy/DimensionalAnalysis"} + "njoy::Log", + {"name": "njoy::dimwits", "remote": "../../njoy/DimensionalAnalysis"} ], "dimwits": [ "catch-adapter", @@ -85,41 +86,41 @@ "hana-adapter": false, "header-utilities": [ "catch-adapter", - "Log" + "njoy::Log" ], "interpolation": [ - {"name": "range-v3", - "remote": "https://github.com/ericniebler/range-v3", + {"name": "range-v3::range-v3", + "remote": "../../ericniebler/range-v3", "tag": "0.11.0"}, - "Log", - "header-utilities", - {"name": "dimwits", "remote": "https://github.com/njoy/DimensionalAnalysis"} + "njoy::Log", + "njoy::header-utilities", + {"name": "njoy::dimwits", "remote": "../../njoy/DimensionalAnalysis"} ], "lipservice": [ "ENDFtk", "catch-adapter", "hana-adapter", {"name": "nlohmann_json", - "remote": "https://github.com/nlohmann/json", + "remote": "../../nlohmann/json", "tag": "v3.7.3", "setup": "set(JSON_BuildTests OFF CACHE INTERNAL \"\")"}, "utility" ], "njoy_c_bindings": [ - {"name": "njoy", "remote": "https://github.com/njoy/NJOY2016"} + {"name": "njoy", "remote": "../../njoy/NJOY2016"} ], "resonanceReconstruction": [ - {"name": "range-v3", - "remote": "https://github.com/ericniebler/range-v3", + {"name": "range-v3::range-v3", + "remote": "../../ericniebler/range-v3", "tag": "0.11.0"}, "ENDFtk", {"name": "eigen", "remote": "https://gitlab.com/libeigen/eigen.git", "tag": "3.3.8", "setup": "set(BUILD_TESTING OFF CACHE BOOL OFF )"}, - "interpolation", - {"name": "dimwits", "remote": "https://github.com/njoy/DimensionalAnalysis"}, - "elementary" + "njoy::interpolation", + {"name": "njoy::dimwits", "remote": "../../njoy/DimensionalAnalysis"}, + "njoy::elementary" ], "tclap-adapter": false, "eigen-adapter": false, @@ -129,14 +130,14 @@ ], "utility": [ "catch-adapter", - "header-utilities" + "njoy::header-utilities" ], "range-v3-adapter": false, "NJOY21": [ "ENDFtk", - {"name": "dimwits", "remote": "https://github.com/njoy/DimensionalAnalysis"} , + {"name": "njoy::dimwits", "remote": "../../njoy/DimensionalAnalysis"} , "lipservice", - {"name": "njoy", "remote": "https://github.com/njoy/NJOY2016"}, + {"name": "njoy", "remote": "../../njoy/NJOY2016"}, "njoy_c_bindings", "tclap-adapter", "utility" diff --git a/devtools/build_system.py b/devtools/build_system.py index 3004941..f043b5d 100644 --- a/devtools/build_system.py +++ b/devtools/build_system.py @@ -56,6 +56,15 @@ def name(self): else: return os.path.basename(self._path) + @property + def testName(self): + """ The name of the interface library used for testing dependencies, derived from the name + + """ + + return self.name + "_testing" + + @property def dependencies(self): """ List of dependencies. @@ -128,16 +137,25 @@ def write_test_list(self): # Setup ####################################################################### - message( STATUS "Adding {} unit testing" ) - enable_testing() + message( STATUS "Adding {0} unit testing" ) + add_library( {1} INTERFACE ) + target_link_libraries( {1} INTERFACE {0} ) + + """.format(self.name, self.testName)) + ) + # this expects catch-adapter to be used for the testing library and should be updated if this is replaced with another lib (e.g. Catch2) + if (any(dependency.name == "catch-adapter" for dependency in self.dependencies)): + f.write('target_link_libraries({0} INTERFACE catch-adapter)\n'.format(self.testName)) + + f.write(dedent(""" ####################################################################### # Unit testing directories ####################################################################### - """.format(self.name)) - ) + """)) + for dir_ in test_directories: f.write('add_subdirectory( {} )\n'.format(dir_)) @@ -159,9 +177,15 @@ def write_cmakelists(self): ######################################################################## # Preamble ######################################################################## + + set(subproject OFF) + if(DEFINED PROJECT_NAME) + set(subproject ON) + endif() - cmake_minimum_required( VERSION 3.14 ) + cmake_minimum_required( VERSION 3.24 ) project( {0} LANGUAGES CXX ) + ######################################################################## @@ -171,8 +195,15 @@ def write_cmakelists(self): set( CMAKE_CXX_STANDARD 17 ) set( CMAKE_CXX_STANDARD_REQUIRED YES ) - option( {0}_unit_tests + include(CTest) + include(CMakeDependentOption) + + cmake_dependent_option( {0}_unit_tests "Compile the {0} unit tests and integrate with ctest" ON + BUILD_TESTING AND NOT ${{subproject}} + ) + option( {0}_installation + "Install {0}" ON ) option( strict_compile "Treat all warnings as errors." ON @@ -196,13 +227,11 @@ def write_cmakelists(self): # Dependencies ######################################################################## - set( REPOSITORIES "release" + set( REPOSITORIES "develop" CACHE STRING "Options for where to fetch repositories: develop, release, local" ) - message( STATUS "Using ${{REPOSITORIES}} repositories" ) - if( REPOSITORIES STREQUAL "develop" ) include( cmake/develop_dependencies.cmake ) @@ -223,6 +252,12 @@ def write_cmakelists(self): ######################################################################## # Project targets ######################################################################## + include(GNUInstallDirs) + + string( CONCAT prefix + "$" + "$" + ) """) ) @@ -242,18 +277,33 @@ def write_cmakelists(self): else: for file_ in self._tree.list_compiled_source(): f.write('\n {}'.format(file_)) - f.write('\n )\n') + f.write('\n )\n\n') + + f.write('add_library( njoy::{0} ALIAS {0} )\n\n'.format(self.name)) f.write( - 'target_include_directories( {0} {1} src/ )\n' + 'target_include_directories( {0} {1} ${{prefix}} )\n\n' ''.format(self.name, link_type) ) - if self.dependencies: + # see if the dependencies list needs to be written + need_to_write_link_libraries_list = (self.dependencies and + any(dependency.name != "spdlog" and dependency.name != "catch-adapter" for dependency in self.dependencies)) + + if need_to_write_link_libraries_list: f.write('target_link_libraries( {}\n'.format(self.name)) for d in self.dependencies: - f.write(' {0} {1}\n'.format(link_type, d.name)) - f.write(' )\n') + if (d.name != "spdlog" and d.name != "catch-adapter"): + f.write(' {0} {1}\n'.format(link_type, d.libName)) + f.write(' )\n\n') + + if (any(dependency.name == "spdlog" for dependency in self.dependencies)): + f.write('# treat spdlog specially due to mixed namespace usage\n') + f.write('if (TARGET spdlog::spdlog)\n') + f.write(' target_link_libraries({0} {1} spdlog::spdlog)\n'.format(self.name, link_type)) + f.write('else()\n') + f.write(' target_link_libraries({0} {1} spdlog)\n'.format(self.name, link_type)) + f.write('endif()\n\n') if not self._tree.header_only: f.write(dedent("""\ @@ -277,7 +327,7 @@ def write_cmakelists(self): if( CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR ) # unit testing - if( {}_unit_tests ) + if( ${{{}_unit_tests}} ) include( cmake/unit_testing.cmake ) endif() @@ -285,6 +335,55 @@ def write_cmakelists(self): """.format(self.name)) ) + f.write(dedent("""\ + ####################################################################### + # Installation + ####################################################################### + + if({0}_installation) + include(CMakePackageConfigHelpers) + + install(TARGETS {0} EXPORT {0}-targets + ARCHIVE DESTINATION "${{CMAKE_INSTALL_LIBDIR}}" + LIBRARY DESTINATION "${{CMAKE_INSTALL_LIBDIR}}" + RUNTIME DESTINATION "${{CMAKE_INSTALL_BINDIR}}" + ) + + install(EXPORT {0}-targets + FILE "{0}-targets.cmake" + NAMESPACE njoy:: + DESTINATION share/cmake/{0} + ) + + string(TOLOWER {0} lowercasePackageName) + + configure_package_config_file( + ${{CMAKE_CURRENT_SOURCE_DIR}}/cmake/${{lowercasePackageName}}-config.cmake.in + ${{PROJECT_BINARY_DIR}}/${{lowercasePackageName}}-config.cmake + INSTALL_DESTINATION share/cmake/{0} + ) + + install(DIRECTORY src/ + DESTINATION "${{CMAKE_INSTALL_INCLUDEDIR}}" + FILES_MATCHING PATTERN "*.hpp" + PATTERN "*test*" EXCLUDE + ) + + install(FILES + "${{PROJECT_BINARY_DIR}}/${{lowercasePackageName}}-config.cmake" + DESTINATION share/cmake/{0} + ) + + if(NOT subproject) + set(CPACK_PACKAGE_VENDOR "Los Alamos National Laboratory") + set(CPACK_RESOURCE_FILE_LICENSE "${{CMAKE_CURRENT_SOURCE_DIR}}/LICENSE") + include(CPack) + endif() + endif() + + """.format(self.name)) + ) + # close file f.close() @@ -312,10 +411,37 @@ def write_dependencies(self): os.path.join( self._path, 'cmake', - filename - ) + filename, + ), + self.name + ) + + def write_installation_dependencies(self): + filename = os.path.join( + self._path, + 'cmake', + "{}-config.cmake.in".format(self.name.lower()) ) + with open(filename, 'w') as f: + f.write("include(CMakeFindDependencyMacro)\n\n") + + for d in self.dependencies: + # Don't include catch as an installation dependency + if (d.name != "catch-adapter"): + # Treat spdlog specially since its installed target is namespaced + if (d.name == "spdlog"): + f.write('if (NOT TARGET spdlog::spdlog)\n') + f.write(' find_dependency(spdlog)\n') + f.write('endif()\n\n') + else: + f.write('if (NOT TARGET {0})\n'.format(d.libName)) + f.write(' find_dependency({0})\n'.format(d.packageName)) + f.write('endif()\n\n') + + f.write("""include("${{CMAKE_CURRENT_LIST_DIR}}/{}-targets.cmake")""".format(self.name)) + + ################################################################### # Private functions @@ -394,7 +520,7 @@ def _one_test(self, dir_): target_link_libraries( {0}.test PUBLIC {1} ) - """.format(testname, self.name)) + """.format(testname, self.testName)) ) # compile options diff --git a/devtools/dependencies/dependencies.py b/devtools/dependencies/dependencies.py index 5216b70..7888525 100644 --- a/devtools/dependencies/dependencies.py +++ b/devtools/dependencies/dependencies.py @@ -58,7 +58,7 @@ def add_dependencies(self, *args): 'Cannot register an object other than a Dependency.') self._dependencies.append(dep) - def cmake_file(self, filename): + def cmake_file(self, filename, libName): """ Write the dependency information to a CMake file. """ @@ -67,8 +67,9 @@ def cmake_file(self, filename): # preamble f.write(dedent("""\ - cmake_minimum_required( VERSION 3.14 ) - include( FetchContent ) + cmake_minimum_required( VERSION 3.24 ) + list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/.cmake) + include( shacl_FetchContent ) """) ) @@ -91,12 +92,19 @@ def cmake_file(self, filename): # Load dependencies ####################################################################### - FetchContent_MakeAvailable( + shacl_FetchContent_MakeAvailable( """) ) for dependency in self.dependencies: - f.write(' {}\n'.format(dependency.name)) - f.write(' )\n') + if (dependency.name != "catch-adapter"): + f.write(' {}\n'.format(dependency.packageName)) + f.write(' )\n\n') + + # Only look for testing library if testing is enabled + if (any(dependency.name == "catch-adapter" for dependency in self.dependencies)): + f.write('if (${{{0}_unit_tests}})\n'.format(libName)) + f.write(' shacl_FetchContent_MakeAvailable(catch-adapter)\n') + f.write('endif()\n\n') f.close() @@ -106,4 +114,4 @@ def cmake_file(self, filename): d = Dependencies() d.add_dependencies(d1, d2) - d.cmake_file('blah.cmake') + d.cmake_file('blah.cmake', 'libraryName') diff --git a/devtools/dependencies/dependency.py b/devtools/dependencies/dependency.py index aa79c9e..70d675a 100644 --- a/devtools/dependencies/dependency.py +++ b/devtools/dependencies/dependency.py @@ -39,7 +39,7 @@ def __init__(self, @property def name(self): - """ The name of the repository, as referenced in the build system. + """ The name of the library, as referenced in dependencies.json. Typically, this is unnecessary to set, as the name can be implied from the remote. But, for example, the repostiory @@ -52,6 +52,35 @@ def name(self): else: return os.path.basename(self.remote) + @property + def libName(self): + """ The name of the library, as referenced in the build system. + + This is the name used for linking and is typically namespaced. + E.g. njoy::dimwits + + """ + + if self._name: + return self._name + else: + return "njoy::" + os.path.basename(self.remote) + + @property + def packageName(self): + """ The name of the package, as referenced in the build system. + + This is the name used for calls to find_package and is + either not namespaced or contains the namespace with a dash instead of semicolons. + E.g. dimwits instead of njoy::dimwits. + + """ + + if self._name: + return self._name.split(':')[-1] + else: + return os.path.basename(self.remote) + @name.setter def name(self, value: str): self._name = value @@ -60,10 +89,10 @@ def name(self, value: str): def remote(self): """ The URL to the remote repository location. - This is typically a URL to GitHub, but there are of course - other places one can use. + This is typically a URL relative to where the repository is hosted, but of + course not all repos are hosted on the same server. - If a name is given but not a remote, the NJOY GitHub project + If a name is given but not a remote, the NJOY project is assumed to be the location of the repository. If no name is given, a remote must be provided. @@ -75,7 +104,7 @@ def remote(self): if not self._name: raise Exception( 'Dependency must have name and/or remote defined.') - return 'https://github.com/njoy/{}'.format(self.name) + return '../../njoy/{}'.format(self.packageName) @remote.setter def remote(self, value: str): @@ -138,11 +167,11 @@ def fetchcontent_declare(self): """ result = dedent("""\ - FetchContent_Declare( {name} + shacl_FetchContent_Declare( {packageName} GIT_REPOSITORY {remote} GIT_TAG {tag} """).format( - name=self.name, + packageName=self.packageName, remote=self.remote, tag=self.tag ) diff --git a/update_repository.py b/update_repository.py index a739aed..7159e83 100644 --- a/update_repository.py +++ b/update_repository.py @@ -90,30 +90,31 @@ def make_build_system(args): args.name ) + + # Dependencies are given in an input JSON file + with open(args.dependencies, 'r') as f: + dependencies = json.load(f) + + deps = dependencies[b.name] + if deps: + b.dependencies = deps + if args.release: - # Release dependencies are taken from examining the - # build/_deps folder - - b.dependencies = ReleaseDependencies( - os.path.join( - args.path, - args.build_dir, - '_deps' - ) + # Release dependencies are the same as the develop dependencies + # but with the hashes taken from examining the + # checked-out hash in the build/_deps folder + deps_path = os.path.join( + args.path, + args.build_dir, + '_deps' ) + b.dependencies = ReleaseDependencies(deps_path) - else: - # Develop dependencies are given in an input JSON file - - with open(args.dependencies, 'r') as f: - dependencies = json.load(f) - deps = dependencies[b.name] - if deps: - b.dependencies = deps b.write_dependencies() if not args.release: + b.write_installation_dependencies() b.write_cmakelists() b.write_test_list()