├── .gitignore ├── CMakeLists.txt ├── cmake ├── ConanConfig.cmake └── libman.cmake ├── conanfile.py ├── data ├── CMakeLists.txt └── spec.bs ├── docs ├── .gitignore ├── CMakeLists.txt ├── _static │ └── custom.css ├── conf.py ├── files.rst ├── format.rst ├── index-file.rst ├── index.rst ├── intro.rst ├── library-file.rst ├── overview.rst └── package-file.rst ├── extras ├── CMakeLists.txt └── libman-py │ ├── .gitignore │ ├── .pylintrc │ ├── CMakeLists.txt │ ├── Pipfile │ ├── Pipfile.lock │ ├── libman │ ├── __init__.py │ ├── __main__.py │ ├── data.py │ ├── data_test.py │ ├── main.py │ ├── parse.py │ ├── parse_test.py │ └── util.py │ ├── mypy.ini │ ├── setup.py │ └── tox.ini ├── lm_conan ├── __init__.py ├── cmake.py ├── data.py ├── export.py ├── generator.py └── vs.py ├── pmm.cmake ├── src ├── CMakeLists.txt └── lm │ ├── lm.cpp │ └── lm.hpp ├── test_package ├── CMakeLists.txt ├── LibManTest.lmp.in ├── conanfile.py ├── something.cpp ├── something.lml.in └── src │ └── foo │ └── bar.hpp └── tests ├── CMakeLists.txt ├── RunProjectTest.cmake ├── export-simple ├── CMakeLists.txt └── src │ ├── simple.cpp │ └── simple.hpp ├── import-empty ├── CMakeLists.txt └── INDEX.lmi ├── import-export ├── RunTest.cmake ├── app │ ├── CMakeLists.txt │ └── main.cpp └── library │ ├── CMakeLists.txt │ └── src │ └── say │ ├── hello.cpp │ └── hello.hpp ├── import-simple ├── CMakeLists.txt ├── INDEX.lmi ├── main.cpp └── subdir │ ├── header-dir │ └── super-simple-lib.hpp │ ├── simple-lib.lml │ └── simple.lmp ├── just-include └── CMakeLists.txt ├── my_test.cpp └── test-main.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .mypy_cache/ 3 | .pytest_cache/ 4 | _pmm/ 5 | .python-version -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_CURRENT_SOURCE_DIR}/cmake") 4 | 5 | project(libman 6 | VERSION 0.0.1 7 | DESCRIPTION "A portable way to represent libraries in a dependency tree" 8 | ) 9 | 10 | include(libman) 11 | 12 | # Global options 13 | 14 | # Randomly pick between using vcpkg or Conan, to ensure that we are always able 15 | # to build with either 16 | include(pmm.cmake) 17 | string(RANDOM LENGTH 1 ALPHABET 01 use_vcpkg) 18 | if((use_vcpkg OR LIBMAN_USE_VCPKG) AND NOT CONAN_EXPORTED) 19 | pmm(VCPKG 20 | REVISION 43deeaf0c8b6086310ee753be2e93c941f7ffd75 21 | REQUIRES catch2 22 | ) 23 | else() 24 | pmm(CONAN) 25 | conan_set_find_paths() 26 | endif() 27 | 28 | # We also want the community modules 29 | pmm(CMakeCM ROLLING) 30 | 31 | find_package(Catch2 REQUIRED) 32 | 33 | option(BUILD_SPEC "Build the specification document" ON) 34 | if(BUILD_SPEC) 35 | find_package(Bikeshed) 36 | endif() 37 | add_subdirectory(data) 38 | 39 | 40 | add_subdirectory(src) 41 | 42 | option(BUILD_TESTING "Build tests" ON) 43 | if(BUILD_TESTING AND (PROJECT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)) 44 | enable_testing() 45 | add_subdirectory(tests) 46 | endif() 47 | 48 | add_subdirectory(extras) 49 | 50 | option(BUILD_DOCS "Build the documentation" ON) 51 | if(BUILD_DOCS) 52 | add_subdirectory(docs) 53 | endif() 54 | 55 | export_package(NAMESPACE libman) 56 | export_library(libman HEADER_ROOTS src) 57 | -------------------------------------------------------------------------------- /cmake/ConanConfig.cmake: -------------------------------------------------------------------------------- 1 | set(BUILD_SPEC FALSE CACHE BOOL "Don't build the spec in Conan" FORCE) 2 | set(BUILD_DOCS FALSE CACHE BOOL "Don't build the docs in Conan" FORCE) 3 | set(BUILD_PYTHON_LIB FALSE CACHE BOOL "Don't build the Python library in Conan" FORCE) -------------------------------------------------------------------------------- /cmake/libman.cmake: -------------------------------------------------------------------------------- 1 | ## This module defines a libman-compliant importer for CMake, all through the 2 | ## `import_packages()` function (defined at the bottom of this file) 3 | ## 4 | ## This module does some trickery to increase performance, mainly by reducing 5 | ## the frequency with which we must read the libman manifests using timestamp 6 | ## checking based on "cache" files, wherein the content of a libman manifest 7 | ## is converted to a roughly equivalent CMake file that is directly include()'d 8 | ## to get the data. 9 | ## 10 | ## For example, when we wish to read the libman index, we generate a unique and 11 | ## deterministic destination for a .cmake file based on the hash of the filepath 12 | ## of the index. This file is the "index cache." 13 | ## 14 | ## (This file has nothing to do with the CMakeCache.txt format, although we 15 | ## may wish to someday further optimize via some `load_cache()` trickery.) 16 | ## 17 | ## The "cache files" set global properties with information that was contained 18 | ## in the original libman manifest. The cache files for libraries will also 19 | ## define the imported targets to which a user will link. 20 | ## 21 | ## In addition, the cache files are generated as-needed, so there is no need to 22 | ## fear generating an enormous index. 23 | ## 24 | ## 25 | ## The cache file for the libman index sets the following properties: 26 | ## 27 | ## - libman_PACKAGES : A list of the packages which the index provides 28 | ## - libman_PACKAGES/::Path : The path to the .lmp file for the 29 | ## package. Each package receives a global property of this format 30 | ## 31 | ## 32 | ## The cache file for packages (.lmp files) set the following properties: 33 | ## 34 | ## - libman_PACKAGES/::Namepsace : The package 'Namespace' field 35 | ## - libman_PACKAGES/::Requires : List of packages that must be imported 36 | ## - libman_PACKAGES/::Libraries : List of the paths to `.lml` files 37 | ## - libman_PACKAGES/::_Imported : Not from the manifest. FALSE until 38 | ## the import finishes. Used to aide in dependency resolution for Requires 39 | ## 40 | ## 41 | ## The cache file for libraries (.lml files) does not set any properties, but 42 | ## instead creates the import targets for the libraries of the package. 43 | 44 | 45 | # A variable to note the path to this file 46 | set(LIBMAN_CMAKE_SCRIPT "${CMAKE_CURRENT_LIST_FILE}") 47 | 48 | if(NOT CMAKE_SCRIPT_MODE_FILE) 49 | define_property(TARGET 50 | PROPERTY libman_EXPORT_PROPERTIES 51 | INHERITED 52 | BRIEF_DOCS "Properties to export in the libman file" 53 | FULL_DOCS "This adds X-CMake-Property lines to the libman library manifest for each property listed." 54 | ) 55 | set_property(DIRECTORY PROPERTY libman_EXPORT_PROPERTIES "") 56 | endif() 57 | 58 | ## Rewrites a path to be absolute, based on the directory containing `filepath` 59 | # NOTE: `filepath` is a _file_ path, not a directory path. The filename will be 60 | # stripped to get the containing directory. 61 | function(_lm_resolve_path_from_file path_var filepath) 62 | # If it is already absolute, nothing to do. 63 | if(IS_ABSOLUTE "${${path_var}}") 64 | return() 65 | endif() 66 | # Get the directory containg `filepath`, and resolve the given path var 67 | # as being relative to that directory. 68 | get_filename_component(dirname "${filepath}" DIRECTORY) 69 | get_filename_component(abspath "${dirname}/${${path_var}}" ABSOLUTE) 70 | set(${path_var} "${abspath}" PARENT_SCOPE) 71 | endfunction() 72 | 73 | 74 | ## Parse a libman-format manifest file, given a prefix and a path to a file to 75 | ## parse. 76 | ## 77 | ## All of the keys within the file create variables in the caller scope of the 78 | ## format __. The variable is set to the list of keys 79 | ## that were parsed, allowing key iteration. 80 | function(_lm_parse_file prefix filepath) 81 | file(READ "${filepath}" content) 82 | # Escape ';' in the file to prevent individual lines from splitting 83 | string(REPLACE ";" "\\;" content "${content}") 84 | # And now split the lines 85 | string(REPLACE "\n" ";" lines "${content}") 86 | # We read the file, so we depend on it 87 | set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${filepath}") 88 | # Clear the value of the prefix var 89 | set(${prefix}) 90 | # Iterate the lines are parse each one 91 | foreach(line IN LISTS lines) 92 | # Whitespace is not significant 93 | string(STRIP "${line}" line) 94 | # Skip empty lines and comment lines 95 | if(line STREQUAL "" OR line MATCHES "^#") 96 | continue() 97 | endif() 98 | # Parse the key-value pairs 99 | if(NOT line MATCHES "^([^ \t]+):[ \t]+(.*)$") 100 | # It may be a key and value ^ 101 | # ... or a key with an empty string: 102 | if(NOT line MATCHES "^([^ \t]):()$") 103 | message(WARNING "Invalid line in ${filepath}: ${line}") 104 | continue() 105 | endif() 106 | endif() 107 | # Strip the key and value in addition to the line to eat whitespace 108 | # around the ':' 109 | string(STRIP "${CMAKE_MATCH_1}" key) 110 | string(STRIP "${CMAKE_MATCH_2}" val) 111 | if(NOT key IN_LIST ${prefix}) 112 | set(${prefix}__${key}) 113 | list(APPEND ${prefix} "${key}") 114 | endif() 115 | # Escape the semicolon in the value to keep it as a single list item 116 | string(REPLACE ";" "\\;" val "${val}") 117 | list(APPEND ${prefix}__${key} "${val}") 118 | endforeach() 119 | # Send our parse result to the parent 120 | set(${prefix} "${${prefix}}" PARENT_SCOPE) 121 | foreach(key IN LISTS ${prefix}) 122 | set(${prefix}__${key} "${${prefix}__${key}}" PARENT_SCOPE) 123 | endforeach() 124 | endfunction() 125 | 126 | 127 | ## Import the library from the .lml file at `lib_path`, within the given `pkg`, 128 | ## with a name qualified with `namespace`. 129 | function(_lm_import_lib pkg namespace lib_path) 130 | # Hash the filepath to get a unique ID for the actual import file. 131 | string(MD5 lib_path_hash "${lib_path}") 132 | string(SUBSTRING "${lib_path_hash}" 0 6 lib_path_hash) 133 | # Also use the stem from the original path so that we have something that's 134 | # actually readable in the file browser 135 | get_filename_component(stem "${lib_path}" NAME_WE) 136 | set(lib_cmake_file "${__lm_cache_dir}/pkgs/${pkg}/${stem}-${lib_path_hash}.cmake") 137 | # Generate the file only if it doesn't exist, or is older than the 138 | # .lml file being imported from. 139 | if("${lib_path}" IS_NEWER_THAN "${lib_cmake_file}") 140 | _lm_parse_file(lib "${lib_path}") 141 | if(NOT lib__Type STREQUAL "Library") 142 | message(WARNING "Wrong type for library file (${lib_path}): '${lib__Type}'") 143 | endif() 144 | if(NOT lib__Name) 145 | message(FATAL_ERROR "Library file did not provide a name: ${lib_path}") 146 | endif() 147 | # The header 148 | file(WRITE 149 | "${lib_cmake_file}.tmp" 150 | "## DO NOT EDIT.\n" 151 | "# This file was generated by libman from ${lib_path}\n\n" 152 | "# This file defines the import for '${lib__Name}' from ${pkg}\n" 153 | ) 154 | # Detemine the linkage type 155 | if(lib__Path MATCHES "${CMAKE_STATIC_LIBRARY_SUFFIX}$") 156 | set(linkage STATIC) 157 | elseif(lib__Path MATCHES "${CMAKE_SHARED_LIBRARY_SUFFIX}(\\.[0-9]+)*$") 158 | set(linkage SHARED) 159 | elseif(lib__Path) 160 | message(WARNING "We don't recognize the type of import library: ${lib__Path}") 161 | set(linkage UNKNOWN) 162 | else() 163 | set(linkage INTERFACE) 164 | endif() 165 | # Create the add_library() call 166 | set(target_name "${namespace}::${lib__Name}") 167 | file(APPEND 168 | "${lib_cmake_file}.tmp" 169 | "add_library([[${target_name}]] IMPORTED ${linkage})\n" 170 | "set_property(TARGET [[${target_name}]] PROPERTY libman_OWNING_PACKAGE ${pkg})\n" 171 | "set_property(TARGET [[${target_name}]] PROPERTY libman_QUALIFIED_NAME ${namespace}/${lib__Name})\n\n" 172 | ) 173 | # Set the import location, if applicable 174 | if(lib__Path) 175 | _lm_resolve_path_from_file(lib__Path "${lib_path}") 176 | file(APPEND 177 | "${lib_cmake_file}.tmp" 178 | "# Set the linkable file for the target\n" 179 | "set_property(TARGET [[${target_name}]] PROPERTY IMPORTED_LOCATION [[${lib__Path}]])\n\n" 180 | ) 181 | endif() 182 | # Add the include directories 183 | foreach(inc IN LISTS lib__Include-Path) 184 | _lm_resolve_path_from_file(inc "${lib_path}") 185 | file(APPEND 186 | "${lib_cmake_file}.tmp" 187 | "set_property(TARGET [[${target_name}]] APPEND PROPERTY INTERFACE_INCLUDE_DIRECTORIES [[${inc}]])\n" 188 | ) 189 | endforeach() 190 | # Add the preprocessor definitions (yuck) 191 | foreach(def IN LISTS lib__Preprocessor-Define) 192 | file(APPEND 193 | "${lib_cmake_file}.tmp" 194 | "set_property(TARGET [[${target_name}]] APPEND PROPERTY INTERFACE_COMPILE_DEFINITIONS [[${def}]])\n" 195 | ) 196 | endforeach() 197 | # Add the transitive usage information (interface links) 198 | foreach(use IN LISTS lib__Uses) 199 | if(NOT use MATCHES "^(.+)/(.+)$") 200 | message(FATAL_ERROR "Cannot resolve invalid transitive usage on ${target_name}: ${use}") 201 | continue() 202 | endif() 203 | set(use_ns "${CMAKE_MATCH_1}") 204 | set(use_lib "${CMAKE_MATCH_2}") 205 | file(APPEND 206 | "${lib_cmake_file}.tmp" 207 | "set_property(TARGET [[${target_name}]] APPEND PROPERTY INTERFACE_LINK_LIBRARIES [[${use_ns}::${use_lib}]])\n" 208 | ) 209 | endforeach() 210 | foreach(link IN LISTS lib__Links) 211 | if(NOT use MATCHES "^(.+)/(.+)$") 212 | message(FATAL_ERROR "Cannot resolve invalid transitive usage on ${target_name}: ${use}") 213 | continue() 214 | endif() 215 | set(use_ns "${CMAKE_MATCH_1}") 216 | set(use_lib "${CMAKE_MATCH_2}") 217 | file(APPEND 218 | "${lib_cmake_file}.tmp" 219 | "set_property(TARGET [[${target_name}]] APPEND PROPERTY INTERFACE_LINK_LIBRARIES [[$]])\n" 220 | ) 221 | endforeach() 222 | # Add verbatim links 223 | foreach(link IN LISTS lib__X-CMake-Link) 224 | file(APPEND 225 | "${lib_cmake_file}.tmp" 226 | "set_property(TARGET [[${target_name}]] APPEND PROPERTY INTERFACE_LINK_LIBRARIES [[${link}]])\n" 227 | ) 228 | endforeach() 229 | # Add CMake properties 230 | foreach(propspec IN LISTS lib__X-CMake-Property) 231 | if(NOT propspec MATCHES "([^ ]+) := (.*)") 232 | message(WARNING "Library has invalid X-CMake-Property line: ${propspec}") 233 | continue() 234 | endif() 235 | set(prop_name "${CMAKE_MATCH_1}") 236 | set(prop_val "${CMAKE_MATCH_2}") 237 | file(APPEND 238 | "${lib_cmake_file}.tmp" 239 | "set_property(TARGET [[${target_name}]] APPEND PROPERTY [[${prop_name}]] [[${prop_val}]])\n" 240 | ) 241 | endforeach() 242 | # Add special requirements 243 | foreach(req IN LISTS lib__Special-Uses) 244 | if(req STREQUAL "Threading") 245 | file(APPEND 246 | "${lib_cmake_file}.tmp" 247 | "set_property(TARGET [[${target_name}]] APPEND PROPERTY INTERFACE_LINK_LIBRARIES [[Threads::Threads]])\n" 248 | ) 249 | elseif(req STREQUAL "Sockets") 250 | if(WIN32) 251 | file(APPEND 252 | "${lib_cmake_file}.tmp" 253 | "set_property(TARGET [[${target_name}]] APPEND PROPERTY INTERFACE_LINK_LIBRARIES Ws2_32)\n" 254 | ) 255 | endif() 256 | else() 257 | message(WARNING "Un-implemented special requirement '${req}' for imported target '${target_name}'") 258 | endif() 259 | endforeach() 260 | # Add CMake inclusions 261 | foreach(inc IN LISTS lib__X-CMake-Include) 262 | set(orig "${inc}") 263 | _lm_resolve_path_from_file(inc "${lib_path}") 264 | file(APPEND 265 | "${lib_cmake_file}.tmp" 266 | "\n# Inclusion from X-CMake-Include: ${orig}\n" 267 | "set_property(GLOBAL APPEND PROPERTY _LIBMAN_PENDING_INCLUDES [[${inc}]])\n" 268 | ) 269 | endforeach() 270 | # Commit 271 | file(RENAME "${lib_cmake_file}.tmp" "${lib_cmake_file}") 272 | endif() 273 | # Include the generated file, thereby defining the imported target. This is 274 | # where the magic happens! 275 | include("${lib_cmake_file}") 276 | endfunction() 277 | 278 | 279 | ## Load the information about the package `name` from the already-loaded index. 280 | function(_lm_load_package name) 281 | # Check that the index actually provides the package we asked for 282 | get_cmake_property(packages libman_PACKAGES) 283 | if(NOT name IN_LIST packages) 284 | message(FATAL_ERROR "Cannot import package '${name}': It is not named in the package index") 285 | endif() 286 | # Compute the destination for the cache file 287 | get_cmake_property(pkg_file libman_PACKAGES/${name}::path) 288 | set(pkg_cmake_file "${__lm_cache_dir}/pkgs/${name}.cmake") 289 | # Only generate it if it doesn't exist, or is out-of-date from the .lmp file 290 | if("${pkg_file}" IS_NEWER_THAN "${pkg_cmake_file}") 291 | _lm_parse_file(pkg "${pkg_file}") 292 | if(NOT pkg__Type STREQUAL "Package") 293 | message(WARNING "Wrong type for package file (${pkg_file}): '${pkg__Type}'") 294 | endif() 295 | if(NOT pkg__Name STREQUAL name) 296 | message(WARNING "Package's declared name does not match its name in the index: '${pkg__Name}' (Expected '${name}')") 297 | endif() 298 | if(pkg__Namespace STREQUAL "") 299 | message(FATAL_ERROR "Package '${pkg__Name}' does not declare a package namespace.") 300 | endif() 301 | # The header. 302 | # NOTE: We need to write empty strings for all of the array properties, 303 | # otherwise a call to get_property on that property will return NOTFOUND 304 | # in the case that the array property was never APPENDed to. This might 305 | # be fixable with define_property()? 306 | file(WRITE 307 | "${pkg_cmake_file}.tmp" 308 | "## DO NOT EDIT.\n" 309 | "# This file was generated by libman from ${pkg_file}\n" 310 | "set_property(GLOBAL PROPERTY [[libman_PACKAGES/${name}::Namespace]] [[${pkg__Namespace}]])\n" 311 | "set_property(GLOBAL PROPERTY [[libman_PACKAGES/${name}::Requires]] [[]])\n" 312 | "set_property(GLOBAL PROPERTY [[libman_PACKAGES/${name}::Libraries]] [[]])\n" 313 | "set_property(GLOBAL PROPERTY [[libman_PACKAGES/${name}::_Imported]] FALSE)\n" 314 | ) 315 | # Expose the requirements 316 | foreach(req IN LISTS pkg__Requires) 317 | file(APPEND 318 | "${pkg_cmake_file}.tmp" 319 | "set_property(GLOBAL APPEND PROPERTY [[libman_PACKAGES/${name}::Requires]] [[${req}]])\n" 320 | ) 321 | endforeach() 322 | # Set the path to the .lml files for the package 323 | foreach(lib IN LISTS pkg__Library) 324 | _lm_resolve_path_from_file(lib "${pkg_file}") 325 | file(APPEND 326 | "${pkg_cmake_file}.tmp" 327 | "set_property(GLOBAL APPEND PROPERTY [[libman_PACKAGES/${name}::Libraries]] [[${lib}]])\n" 328 | ) 329 | endforeach() 330 | # CMake inclusions 331 | foreach(inc IN LISTS pkg__X-CMake-Include) 332 | set(orig "${inc}") 333 | _lm_resolve_path_from_file(inc "${pkg_file}") 334 | file(APPEND 335 | "${pkg_cmake_file}.tmp" 336 | "\n# Package inclusion from X-CMake-Include: ${orig}\n" 337 | "set_property(GLOBAL APPEND PROPERTY _LIBMAN_PENDING_INCLUDES [[${inc}]])\n" 338 | ) 339 | endforeach() 340 | # Commit 341 | file(RENAME "${pkg_cmake_file}.tmp" "${pkg_cmake_file}") 342 | endif() 343 | # Include the generated file to set all the global properties 344 | include("${pkg_cmake_file}") 345 | endfunction() 346 | 347 | 348 | ## Load up the index from LIBMAN_INDEX 349 | function(_lm_load_index) 350 | # We haven't loaded the package cache yet (for the current LIBMAN_INDEX) 351 | set(index_file "${__lm_cache_dir}/index.cmake") 352 | # Check if the libman index is newer than the cache file: 353 | if("${LIBMAN_INDEX}" IS_NEWER_THAN "${index_file}") 354 | # Our cache needs updating 355 | _lm_parse_file(index "${LIBMAN_INDEX}") 356 | if(NOT index__Type STREQUAL "Index") 357 | message(SEND_ERROR "Non-index file set for LIBMAN_INDEX: ${LIBMAN_INDEX} (Type: ${index__Type})") 358 | return() 359 | endif() 360 | # Write a header 361 | file(WRITE 362 | "${index_file}.tmp" 363 | "## DO NOT EDIT.\n" 364 | "## This file is generated by libman.cmake to cache the result of the parseing of ${LIBMAN_INDEX}.\n" 365 | "set_property(GLOBAL PROPERTY libman_PACKAGES [[]])\n\n" 366 | ) 367 | # Declare the packages 368 | foreach(pkg IN LISTS index__Package) 369 | if(NOT pkg MATCHES "^([^;]+);([^;]+)$") 370 | message(FATAL_ERROR "Invalid 'Package' entry in index: ${pkg}") 371 | continue() 372 | endif() 373 | # Strip whitespace from the keys around the ';' 374 | string(STRIP "${CMAKE_MATCH_1}" name) 375 | string(STRIP "${CMAKE_MATCH_2}" path) 376 | # Resolve the path to the .lmp 377 | _lm_resolve_path_from_file(path "${LIBMAN_INDEX}") 378 | file(APPEND 379 | "${index_file}.tmp" 380 | "# Package '${name}'\n" 381 | "set_property(GLOBAL APPEND PROPERTY libman_PACKAGES ${name})\n" 382 | "set_property(GLOBAL PROPERTY [[libman_PACKAGES/${name}::path]] [[${path}]])\n\n" 383 | ) 384 | endforeach() 385 | # Commit 386 | file(RENAME "${index_file}.tmp" "${index_file}") 387 | endif() 388 | # Load the index 389 | include("${index_file}") 390 | endfunction() 391 | 392 | 393 | ## "Import" the given pakage. Different from loading, in that it will also 394 | ## import the libraries from the package (and its dependencies) 395 | function(_lm_import_package name) 396 | # Don't pass over a package twice. 397 | get_cmake_property(already_imported libman_PACKAGES/${name}::_Imported) 398 | if(already_imported) 399 | return() 400 | endif() 401 | # Load the package data from the index 402 | _lm_load_package("${name}") 403 | # Import the requirements of the package 404 | get_cmake_property(reqs libman_PACKAGES/${name}::Requires) 405 | foreach(req IN LISTS reqs) 406 | _lm_import_package(${req}) 407 | endforeach() 408 | # Define the import libraries 409 | get_cmake_property(namespace libman_PACKAGES/${name}::Namespace) 410 | get_cmake_property(libs libman_PACKAGES/${name}::Libraries) 411 | foreach(lib IN LISTS libs) 412 | _lm_import_lib("${name}" "${namespace}" "${lib}") 413 | endforeach() 414 | # Remember that we've already imported this package. 415 | set_property(GLOBAL PROPERTY libman_PACKAGES/${name}::_Imported TRUE) 416 | endfunction() 417 | 418 | 419 | ## The only public interface for libman! Name your packages, and we'll import 420 | ## them! You _must_ set the `LIBMAN_INDEX` variable to a path to the libman 421 | ## index. Recommended to let a dependency manager do this for you. 422 | macro(import_packages) 423 | set_property(GLOBAL PROPERTY _LIBMAN_PENDING_INCLUDES "") 424 | _lm_do_import_packages(${ARGN}) 425 | get_cmake_property(__pending_includes _LIBMAN_PENDING_INCLUDES) 426 | foreach(inc IN LISTS __pending_includes) 427 | include("${inc}") 428 | endforeach() 429 | endmacro() 430 | 431 | function(_lm_do_import_packages) 432 | set(looked_for) 433 | if(NOT DEFINED LIBMAN_INDEX) 434 | foreach(cand 435 | CMAKE_CURRENT_BINARY_DIR 436 | PROJECT_BINARY_DIR 437 | CMAKE_BINARY_DIR 438 | CMAKE_CURRENT_SOURCE_DIR 439 | PROJECT_SOURCE_DIR 440 | CMAKE_SOURCE_DIR 441 | ) 442 | set(cand "${${cand}}/INDEX.lmi") 443 | list(APPEND looked_for "${cand}") 444 | if(EXISTS "${cand}") 445 | set(LIBMAN_INDEX "${cand}") 446 | endif() 447 | endforeach() 448 | endif() 449 | if(NOT DEFINED LIBMAN_INDEX) 450 | list(REMOVE_DUPLICATES looked_for) 451 | string(REPLACE ";" ", " looked_for "${looked_for}") 452 | message(FATAL_ERROR "No LIBMAN_INDEX variable defined, and no INDEX.lmi was found (Looked for ${looked_for})") 453 | endif() 454 | get_filename_component(LIBMAN_INDEX "${LIBMAN_INDEX}" ABSOLUTE) 455 | # Get the location of our cached parse 456 | # Hash the index path to generate an in-build directory where we store the 457 | # cache files 458 | string(MD5 path_hash "${LIBMAN_INDEX}") 459 | string(SUBSTRING "${path_hash}" 0 6 __lm_index_path_hash) 460 | get_filename_component(__lm_cache_dir "${CMAKE_BINARY_DIR}/_libman-${__lm_index_path_hash}" ABSOLUTE) 461 | # Load up that index data into global properties 462 | _lm_load_index() 463 | # Now import those packages 464 | foreach(name IN LISTS ARGN) 465 | _lm_import_package("${name}") 466 | endforeach() 467 | endfunction() 468 | 469 | 470 | function(export_package) 471 | set(options 472 | ADD_TO_ALL 473 | ) 474 | set(args 475 | NAMESPACE 476 | NAME 477 | ) 478 | set(list_args 479 | REQUIRES 480 | ) 481 | cmake_parse_arguments(PARSE_ARGV 0 ARG "${options}" "${args}" "${list_args}") 482 | 483 | foreach(arg IN LISTS ARG_UNPARSED_ARGUMENTS) 484 | message(SEND_ERROR "Unknown argument to export_package: ${arg}") 485 | endforeach() 486 | if(DEFINED ARG_UNPARSED_ARGUMENTS) 487 | message(FATAL_ERROR "Invalid arguments to export_package()") 488 | endif() 489 | 490 | if(NOT TARGET libman-export) 491 | add_custom_target(libman-export) 492 | endif() 493 | 494 | if(NOT ARG_NAME) 495 | set(ARG_NAME "${PROJECT_NAME}") 496 | endif() 497 | if(NOT ARG_NAMESPACE) 498 | set(ARG_NAMESPACE "${ARG_NAME}") 499 | endif() 500 | 501 | set(all_arg) 502 | if(ARG_ADD_TO_ALL) 503 | set(all_arg ALL) 504 | endif() 505 | 506 | set(target __libman-export-package-${ARG_NAME}) 507 | if(TARGET "${target}") 508 | message(FATAL_ERROR "export_package() for ${ARG_NAME} called multiple times") 509 | endif() 510 | add_custom_target(${target} ${all_arg}) 511 | add_dependencies(libman-export ${target}) 512 | 513 | get_filename_component(__export_root "${CMAKE_BINARY_DIR}/${ARG_NAME}.libman-export" ABSOLUTE) 514 | set_target_properties("${target}" PROPERTIES 515 | EXPORT_ROOT "${__export_root}" 516 | NAMESPACE "${ARG_NAMESPACE}" 517 | REQUIRES "${ARG_REQUIRES}" 518 | ) 519 | 520 | # Create the root directory for the export 521 | file(REMOVE_RECURSE "${__export_root}") 522 | 523 | # Generate the head of the file 524 | set(req_genex $) 525 | set(has_requirements $) 526 | set(req_list "$") 527 | set(req_lines "$<${has_requirements}:Requires: ${req_list}>") 528 | set(lib_genex $) 529 | set(has_libraries $) 530 | set(lib_list "$") 531 | set(lib_lines "$<${has_libraries}:Library: ${lib_list}>") 532 | string(CONFIGURE [[ 533 | # This file was generated by `export_package` from CMake for ${PROJECT_NAME} ${PROJECT_VERSION} 534 | Type: Package 535 | Name: ${ARG_NAME} 536 | Namespace: ${ARG_NAMESPACE} 537 | ${req_lines} 538 | ${lib_lines} 539 | ]] lmp_content) 540 | set(_lm_dir "${CMAKE_CURRENT_BINARY_DIR}/_libman") 541 | set(__lmp_path_tmpl "${_lm_dir}/${ARG_NAME}.lmp.in") 542 | file(WRITE "${__lmp_path_tmpl}" "${lmp_content}") 543 | 544 | file(GENERATE OUTPUT "${__export_root}/${ARG_NAME}.lmp" INPUT "${__lmp_path_tmpl}") 545 | endfunction() 546 | 547 | 548 | function(_lm_target_name_genex out target) 549 | set(lm_name $) 550 | set(output_name $) 551 | set(default_name "$,${output_name},${target}>") 552 | set(name_genex $,${lm_name},${default_name}>) 553 | set("${out}" "${name_genex}" PARENT_SCOPE) 554 | endfunction() 555 | 556 | 557 | function(export_library exp_target) 558 | set(options) 559 | set(args 560 | NAME 561 | PACKAGE 562 | ) 563 | set(list_args 564 | HEADER_ROOTS 565 | HEADER_PATTERNS 566 | EXTRA_HEADER_PATTERNS 567 | ) 568 | cmake_parse_arguments(PARSE_ARGV 0 ARG "${options}" "${args}" "${list_args}") 569 | 570 | # Fill in the header patterns we will be copying 571 | if(NOT ARG_HEADER_PATTERNS) 572 | set(ARG_HEADER_PATTERNS 573 | *.h 574 | *.hh 575 | *.hpp 576 | *.h++ 577 | *.inc 578 | *.inl 579 | *.H 580 | *.tpp 581 | ) 582 | endif() 583 | set(header_patterns ${ARG_HEADER_PATTERNS} ${ARG_EXTRA_HEADER_PATTERNS}) 584 | 585 | # Default package name to the project name 586 | if(NOT ARG_PACKAGE) 587 | set(ARG_PACKAGE "${PROJECT_NAME}") 588 | endif() 589 | 590 | # Default the name to the target name 591 | if(NOT ARG_NAME) 592 | set(ARG_NAME "${exp_target}") 593 | endif() 594 | 595 | # Sanity checks on the caller 596 | if(NOT TARGET "${exp_target}") 597 | message(SEND_ERROR "export_library() requires a library target argument") 598 | return() 599 | endif() 600 | get_target_property(imported "${exp_target}" IMPORTED) 601 | if(imported) 602 | message(SEND_ERROR "Exporting an imported target ('${exp_target}') is not allowed") 603 | return() 604 | endif() 605 | get_target_property(type "${exp_target}" TYPE) 606 | set(path_genex) 607 | if(type MATCHES "^((STATIC|SHARED|MODULE)_LIBRARY)$") 608 | set(libdir lib) 609 | if(type MATCHES "^(SHARED|MODULE)" AND CMAKE_SHARED_LIBRARY_SUFFIX STREQUAL ".dll") 610 | # Main lib path goes in `bin/` for DLLs 611 | set(libdir bin) 612 | endif() 613 | set(path_genex ${libdir}/$) 614 | elseif(type STREQUAL "OBJECT_LIBRARY") 615 | message(SEND_ERROR "Exporting of OBJECT libraries is not supported") 616 | return() 617 | elseif(type STREQUAL "EXECUTABLE") 618 | message(SEND_ERROR "Exporting of executables is not supported") 619 | return() 620 | endif() 621 | 622 | # Check that we are actually part of a real package 623 | set(pkg_target __libman-export-package-${ARG_PACKAGE}) 624 | if(NOT TARGET "${pkg_target}") 625 | message(FATAL_ERROR "Cannot export library into non-existent package '${ARG_PACKAGE}' (did you call `export_package()` before `export_library`?) (Check the PACKAGE argument)") 626 | endif() 627 | 628 | # Generate the target that will do the export 629 | set(lm_target __libman-export-library-${ARG_NAME}) 630 | if(TARGET "${lm_target}") 631 | message(FATAL_ERROR "Cannot generate an export for ${ARG_NAME} (Name is already exported)") 632 | endif() 633 | add_custom_target("${lm_target}") 634 | add_dependencies("${pkg_target}" "${lm_target}") 635 | # Temporary dir 636 | set(_lm_dir "${CMAKE_CURRENT_BINARY_DIR}/_libman") 637 | 638 | # Get properties from the package 639 | get_target_property(__export_root "${pkg_target}" EXPORT_ROOT) 640 | get_target_property(namespace "${pkg_target}" NAMESPACE) 641 | get_target_property(already_exported "${pkg_target}" EXPORTED_TARGETS) 642 | set(lib_infix "libs/${namespace}/${ARG_NAME}") 643 | set(lib_root "${__export_root}/${lib_infix}") 644 | 645 | # Add this library to the package listing 646 | set_property( 647 | TARGET "${pkg_target}" 648 | APPEND PROPERTY LIBRARIES 649 | "${lib_infix}/${ARG_NAME}.lml" 650 | ) 651 | set_property( 652 | TARGET "${pkg_target}" 653 | APPEND PROPERTY EXPORTED_TARGETS 654 | ${exp_target} 655 | ) 656 | 657 | # Generate targets which will install the headers into the export directory 658 | set(__has_headers FALSE) 659 | foreach(dir IN LISTS ARG_HEADER_ROOTS) 660 | set(__has_headers TRUE) 661 | get_filename_component(dir "${dir}" ABSOLUTE) 662 | string(MD5 dir_hash "${dir}") 663 | string(SUBSTRING "${dir_hash}" 0 6 dir_hash) 664 | set(stamp_file "${CMAKE_CURRENT_BINARY_DIR}/_libman/headers-${dir_hash}.stamp") 665 | if(EXISTS "${stamp_file}") 666 | file(REMOVE "${stamp_file}") 667 | endif() 668 | set(existing_headers) 669 | foreach(pat IN LISTS header_patterns) 670 | file(GLOB_RECURSE more_headers CONFIGURE_DEPENDS "${dir}/${pat}") 671 | list(APPEND existing_headers ${more_headers}) 672 | endforeach() 673 | add_custom_command( 674 | OUTPUT "${stamp_file}" 675 | DEPENDS ${ARG_HEADERS_DEPENDS} ${existing_headers} 676 | COMMAND "${CMAKE_COMMAND}" 677 | -D "__LIBMAN_INSTALL_HEADERS_MODE=TRUE" 678 | -D "HEADER_ROOT=${dir}" 679 | -D "PATTERNS=${header_patterns}" 680 | -D "DESTINATION=${lib_root}/include" 681 | -P "${LIBMAN_CMAKE_SCRIPT}" 682 | COMMAND "${CMAKE_COMMAND}" -E touch "${stamp_file}" 683 | COMMENT "Copying headers from ${dir} to libman export" 684 | VERBATIM 685 | ) 686 | add_custom_target(libman-export-headers-${dir_hash} DEPENDS "${stamp_file}") 687 | add_dependencies("${lm_target}" libman-export-headers-${dir_hash}) 688 | endforeach() 689 | 690 | # Generate a target that will copy the file to the export root 691 | set(stamp_file "${CMAKE_CURRENT_BINARY_DIR}/_libman/copy-${exp_target}.stamp") 692 | add_custom_command( 693 | OUTPUT "${stamp_file}" 694 | DEPENDS ${exp_target} 695 | COMMAND "${CMAKE_COMMAND}" -E make_directory "${lib_root}/${libdir}" 696 | COMMAND "${CMAKE_COMMAND}" -E copy 697 | "$" 698 | "${lib_root}/${libdir}/" 699 | COMMAND "${CMAKE_COMMAND}" -E touch "${stamp_file}" 700 | COMMENT "Copy library ${exp_target} to libman export root" 701 | ) 702 | add_custom_target(__libman-export-copy-${exp_target} DEPENDS "${stamp_file}") 703 | add_dependencies(${lm_target} __libman-export-copy-${exp_target}) 704 | 705 | # Get the export name of the library 706 | _lm_target_name_genex(name_to_write "${exp_target}") 707 | 708 | # Generate the library file head 709 | set(__lml_tmpl "${_lm_dir}/${target}.lml.in") 710 | file(WRITE "${__lml_tmpl}" "") 711 | file(APPEND "${__lml_tmpl}" "Type: Library\n") 712 | file(APPEND "${__lml_tmpl}" "Name: ${name_to_write}\n") 713 | if(__has_headers) 714 | file(APPEND "${__lml_tmpl}" "Include-Path: include\n") 715 | endif() 716 | 717 | # The path to the compiled binary 718 | if(path_genex) 719 | file(APPEND "${__lml_tmpl}" "Path: ${path_genex}\n") 720 | endif() 721 | 722 | # Export the compile definitions for the library 723 | set(defines "$") 724 | set(def_join "$") 725 | set(defines_line "$<$:Preprocessor-Define: ${def_join}\n>") 726 | file(APPEND "${__lml_tmpl}" "${defines_line}") 727 | 728 | # The big one: Export the transitive usage requirements 729 | get_target_property(libs "${exp_target}" INTERFACE_LINK_LIBRARIES) 730 | if(libs STREQUAL "libs-NOTFOUND") 731 | set(libs) 732 | endif() 733 | set(required_by_usage) 734 | foreach(lib IN LISTS libs) 735 | if(lib MATCHES "^-") 736 | message(WARNING 737 | "export_package() for '${ARG_NAME}' exports target '${exp_target}', " 738 | "which uses link flag '${lib}'. This is not supported and will " 739 | "be omitted from the export information." 740 | ) 741 | continue() 742 | endif() 743 | set(is_link_only FALSE) 744 | if(lib MATCHES "^\\$$") 745 | set(lib "${CMAKE_MATCH_1}") 746 | set(is_link_only TRUE) 747 | endif() 748 | if(NOT TARGET "${lib}") 749 | message(WARNING 750 | "export_package() for '${ARG_NAME}' exports target '${exp_target}', " 751 | "which links to '${lib}', but '${lib}' is not a CMake target. " 752 | "It will be omitted from the export information." 753 | ) 754 | continue() 755 | endif() 756 | get_target_property(lm_qual_name "${lib}" libman_QUALIFIED_NAME) 757 | get_target_property(lm_owning_package "${lib}" libman_OWNING_PACKAGE) 758 | get_target_property(lm_verbatim_link_ok "${lib}" libman_VERBATIM_LINK_OK) 759 | get_target_property(lm_ignore_this "${lib}" libman_IGNORE) 760 | get_target_property(is_imported "${lib}" IMPORTED) 761 | if(lm_ignore_this) 762 | # Do nothing 763 | elseif(TARGET "${lib}" AND NOT is_imported) 764 | if(NOT lib IN_LIST already_exported) 765 | message(WARNING 766 | "export_library(${exp_target}): ${exp_target} links to CMake " 767 | "target '${lib}', but '${lib}' is not already exported. " 768 | "If you are exporting this target, place the export_library(${lib}) " 769 | "call before the export_library(${exp_target}) call. If there " 770 | "is no export_library(${lib}) call, THE GENERATED EXPORT WILL " 771 | "BE UNUSABLE!" 772 | ) 773 | endif() 774 | _lm_target_name_genex(other_name "${lib}") 775 | if(is_link_only) 776 | file(APPEND "${__lml_tmpl}" "Links: ${namespace}/${other_name}") 777 | else() 778 | file(APPEND "${__lml_tmpl}" "Uses: ${namespace}/${other_name}") 779 | endif() 780 | elseif(lib STREQUAL "Threads::Threads") 781 | file(APPEND "${__lml_tmpl}" "Special-Uses: Threading\n") 782 | elseif(lib STREQUAL "std::filesystem") 783 | file(APPEND "${__lml_tmpl}" "Special-Uses: Filesystem\n") 784 | elseif(lm_qual_name) 785 | if(is_link_only) 786 | file(APPEND "${__lml_tmpl}" "Links: ${lm_qual_name}\n") 787 | else() 788 | file(APPEND "${__lml_tmpl}" "Uses: ${lm_qual_name}\n") 789 | endif() 790 | list(APPEND required_by_usage ${lm_owning_package}) 791 | set(required_by_usage "${required_by_usage}" PARENT_SCOPE) 792 | elseif(is_imported) 793 | file(APPEND "${__lml_tmpl}" "X-CMake-Link: ${lib}\n") 794 | if(NOT lm_verbatim_link_ok) 795 | message(STATUS 796 | "NOTE: Exported target '${exp_target}' links to '${lib}', which is " 797 | "an imported library with no associated package information." 798 | ) 799 | message(STATUS 800 | "NOTE: Downstream CMake projects which link to '${exp_target}' will " 801 | "link to '${lib}' verbatim without dependency information." 802 | ) 803 | endif() 804 | else() 805 | message(WARNING 806 | "export_package() for '${ARG_NAME}' exports target '${exp_target}', " 807 | "which links to '${lib}', but '${lib}' is not a CMake target. " 808 | "It will be omitted from the export information." 809 | ) 810 | endif() 811 | endforeach() 812 | 813 | get_target_property(export_props "${exp_target}" libman_EXPORT_PROPERTIES) 814 | foreach(compat_prop 815 | COMPATIBLE_INTERFACE_BOOL 816 | COMPATIBLE_INTERFACE_NUMBER_MAX 817 | COMPATIBLE_INTERFACE_NUMBER_MIN 818 | COMPATIBLE_INTERFACE_STRING 819 | ) 820 | get_target_property(propval "${exp_target}" "${compat_prop}") 821 | if(NOT propval STREQUAL "propval-NOTFOUND") 822 | list(APPEND export_props ${propval}) 823 | endif() 824 | endforeach() 825 | list(REMOVE_DUPLICATES export_props) 826 | foreach(prop IN LISTS export_props compat_string compat_bool compat_num_min compat_num_max) 827 | string(GENEX_STRIP "${prop}" wo_genex) 828 | if(NOT wo_genex STREQUAL prop) 829 | message(WARNING "libman_EXPORT_PROPERTIES does not support generator expressions (Found export of `${prop}`)") 830 | continue() 831 | endif() 832 | set(prop_genex "$") 833 | set(is_empty "$") 834 | set(not_empty "$") 835 | file(APPEND "${__lml_tmpl}" "$<${not_empty}:X-CMake-Property: ${prop} := $\n>") 836 | endforeach() 837 | 838 | # Add the Requires fields to the package 839 | get_target_property(reqs "${pkg_target}" REQUIRES) 840 | list(APPEND reqs "${required_by_usage}") 841 | list(REMOVE_DUPLICATES reqs) 842 | set_property(TARGET "${pkg_target}" PROPERTY REQUIRES "${reqs}") 843 | 844 | # Generate the final lml file 845 | set(lml_path_genex "${lib_root}/${ARG_NAME}.lml") 846 | file(GENERATE 847 | OUTPUT "${lml_path_genex}" 848 | INPUT "${__lml_tmpl}" 849 | ) 850 | endfunction() 851 | 852 | 853 | # Check if we are running in __LIBMAN_INSTALL_HEADERS_MODE 854 | if(__LIBMAN_INSTALL_HEADERS_MODE) 855 | message(STATUS "Exporting headers from project based within '${HEADER_ROOT}'") 856 | set(pattern_args) 857 | foreach(pat IN LISTS PATTERNS) 858 | list(APPEND pattern_args PATTERN ${pat}) 859 | endforeach() 860 | if(NOT IS_DIRECTORY "${HEADER_ROOT}") 861 | message(WARNING "Header root '${HEADER_ROOT}' is not an existing directory") 862 | endif() 863 | file( 864 | INSTALL "${HEADER_ROOT}/" 865 | DESTINATION "${DESTINATION}" 866 | USE_SOURCE_PERMISSIONS 867 | FILES_MATCHING ${pattern_args} 868 | ) 869 | function(_prune_if_empty dirpath) 870 | if(IS_DIRECTORY "${dirpath}") 871 | file(GLOB children "${dirpath}/*") 872 | if(children STREQUAL "") 873 | message(STATUS "Remove empty directory: ${dirpath}") 874 | file(REMOVE "${dirpath}") 875 | get_filename_component(pardir "${dirpath}" DIRECTORY) 876 | _prune_if_empty("${pardir}") 877 | endif() 878 | endif() 879 | endfunction() 880 | message(STATUS "Pruning empty include subdirectories...") 881 | file(GLOB_RECURSE files "${DESTINATION}/*") 882 | file(GLOB_RECURSE dirs LIST_DIRECTORIES true "${DESTINATION}/*") 883 | list(REMOVE_ITEM dirs ${files} ~~~/::~) 884 | foreach(dir IN LISTS dirs) 885 | file(GLOB_RECURSE files "${dir}/*") 886 | # `files` will only contain files, not directories. If this dir has 887 | # any file children, the list will be non-empty 888 | if(files STREQUAL "") 889 | message(STATUS "Removing empty directory: ${dir}") 890 | file(REMOVE_RECURSE "${dir}") 891 | endif() 892 | endforeach() 893 | return() 894 | endif() 895 | 896 | # Define the threading interface lib, used for the `Threading` requirement 897 | find_package(Threads) 898 | -------------------------------------------------------------------------------- /conanfile.py: -------------------------------------------------------------------------------- 1 | import sys 2 | if sys.version_info < (3, 6, 0): 3 | raise ImportError('libman for Conan requires Python 3.6 or newer') 4 | 5 | import conans 6 | 7 | from lm_conan.generator import Generator 8 | 9 | # Imports that are meant to be re-imported by clients 10 | from lm_conan.cmake import CMakeConanFile, cmake_build 11 | from lm_conan.export import package_exports, AlreadyPackaged, ExportCopier 12 | 13 | # Supress "unused import" warnings 14 | _ = (package_exports, AlreadyPackaged, ExportCopier, cmake_build) 15 | 16 | 17 | # The actual class definition is on the base class `lm_conan.generator.Generator` 18 | # This class just forces the generator to be exposed as `LibMan` to consumers 19 | class LibMan(Generator): 20 | pass 21 | 22 | 23 | class ConanFile(CMakeConanFile): 24 | name = 'libman' 25 | version = '0.2.0+dev13' 26 | build_requires = ('catch2/2.3.0@bincrafters/stable', ) 27 | generators = 'cmake' 28 | exports = ('lm_conan/*', ) 29 | exports_sources = ('cmake/libman.cmake', ) 30 | settings = () 31 | 32 | def build(self): 33 | pass 34 | 35 | def package(self): 36 | self.copy('cmake/libman.cmake', keep_path=False) 37 | -------------------------------------------------------------------------------- /data/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | if(BUILD_SPEC) 2 | if(NOT TARGET Bikeshed::Bikeshed) 3 | message(STATUS "No Bikeshed executable, so we will not generate the spec document") 4 | else() 5 | get_filename_component(bs_input "spec.bs" ABSOLUTE) 6 | get_filename_component(bs_html "${PROJECT_BINARY_DIR}/spec.html" ABSOLUTE) 7 | 8 | add_custom_command( 9 | OUTPUT "${bs_html}" 10 | DEPENDS "${bs_input}" 11 | COMMAND Bikeshed::Bikeshed spec "${bs_input}" "${bs_html}" 12 | COMMENT "Rendering specification to ${bs_html}" 13 | ) 14 | add_custom_target(libman-spec DEPENDS "${bs_html}") 15 | add_custom_target(libman-spec-watch 16 | COMMAND Bikeshed::Bikeshed watch "${bs_input}" "${bs_html}" 17 | COMMENT "Watching ${bs_input} for changes" 18 | USES_TERMINAL 19 | ) 20 | endif() 21 | endif() 22 | -------------------------------------------------------------------------------- /data/spec.bs: -------------------------------------------------------------------------------- 1 | 14 | 15 | # Introduction and Overview # {#intro} 16 | 17 | ## Problem Description and Scope ## {#intro.problem} 18 | 19 | C++ is late to the game in terms of a unified and cross-platform solution for 20 | easily distributing native libraries. 21 | 22 | There are “native” / “system” package managers and package formats, such as 23 | `dpkg`/`apt`, `yum`/`rpm`, `pacman`, and MSI that address a core problem of 24 | managing the global state of a given machine, and these packaging formats often 25 | deal with “native” compiled binaries. These solutions are not cross-platform, 26 | and even if they were, none of them are appropriate for the problem at hand. 27 | They are not focused on development and compilation within the C++ ecosystem. 28 | While other languages and ecosystems have tools such as pip, npm, cargo, mix, 29 | and even cpan, C++ has gone a long time with no widely-accepted solution. 30 | 31 | Why is this? 32 | 33 | There have been many attempts, and there are presently several competing 34 | alternatives for solving the package and dependency management problem. Running 35 | in parallel, and solving a (somewhat) orthogonal problem, are several competing 36 | build systems. CMake, Meson, Waf, b2, and countless others have been pining for 37 | the love and attention of the C++ community for years. All of them wildly 38 | incompatible in terms of their package consumption formats. 39 | 40 | This situation presents a unique problem. With lack of a “reference 41 | implementation” of C++, and no singular and completely universal build tool and 42 | format, we have an N:M problem of N package and dependency managers attempting 43 | to work with M build systems. 44 | 45 | This trouble can be broken down in two directions: 46 | 47 | 1. How do I, the build system, inform a package creation and distribution tool 48 | how my project should be built and collected into a distributable unit? 49 | 2. How do I, the dependency manager, inform the build system how it might 50 | consume the packages I've provided to it? 51 | 52 | This paper and the `libman` system described will cover (2). Investigation into 53 | the inverse (yet equally important) problem (1) will not be discussed in detail, 54 | but warrants further discussion. 55 | 56 | Note: This document will use the abbreviated term *PDM* to refer to "package 57 | and dependency manager" tools. 58 | 59 | 60 | ## Usage Requirements ## {#intro.usage-requirements} 61 | 62 | The concept of usage requirements originated from Boost’s b2 build system, and 63 | has been slowly bleeding into general acceptance via CMake. After years of 64 | experience with CMake, and as it has been developing and maturing its 65 | realization of usage requirements and the concept of the “usage interface,” it 66 | is clear that it is a fruitful path forward. As such, `libman` is explicitly 67 | designed around this concept. 68 | 69 | What are “usage requirements” (also known as the “link interface” or “usage 70 | interface”)? 71 | 72 | When we have a “target” (A thing that one wishes to build), we can say that it 73 | “uses” a library. When we “use” a library, we need to inherit certain 74 | attributes thereof which have a direct effect on the way that the final target 75 | will be built. These include, **but are not limited to**: 76 | 77 | - What header search paths do we need? This ensures that the consumer target 78 | is able to #include the files from the library being consumed. 79 | - What files do we need to include in the link phase? This ensures that 80 | entities with external linkage declared in the library’s headers are 81 | available for symbol resolution during the final link. 82 | - What link/compile options are required? In some rare cases, consuming a 83 | library will require that certain options be enabled/disabled on the compile 84 | or link phase. This is not recommended, but is still very common. 85 | - Who else do we need to “use”? Library composition isn’t a new idea, and it 86 | encourages modularity and encapsulation. To ensure that we are able to 87 | consume a library which builds upon another, we need to be sure that we can 88 | also use the transitive dependencies. This recurses through the “usage” 89 | directed graph until we have satisfied all of the usage requirements. 90 | 91 | `libman` defines a platform-agnostic and build-system-agnostic format for 92 | describing these “usage requirements”, including how one should import 93 | dependencies transitively. Any build system which can represent the above 94 | concepts can import `libman` files. Any PDM which can represent the above 95 | concepts can generate `libman` files. 96 | 97 | Therefore, any `libman`-capable build system can be used with any 98 | `libman`-capable package and dependency manager. 99 | 100 | 101 | ## Goals and Non-Goals ## {#intro.goals} 102 | 103 | The following are the explicit goals of `libman` and this document: 104 | 105 | 1. Define a series of file formats which tell a build system how a library is 106 | to be "used" 107 | 2. Define the semantics of how a build system should interact and perform 108 | name-based package and dependency lookup in a deterministic fashion with no 109 | dependence on "ambient" environment state. 110 | 3. Define the requirements from a PDM for generating a correct and coherent 111 | set of `libman` files. 112 | 113 | Perhaps just as important as the goals are the non-goals. In particular, 114 | `libman` **does not** seek to do any of the following: 115 | 116 | 1. Define the semantics of ABI and version compatibility between libraries 117 | 2. Facilitate dependency resolution beyond trivial name-based path lookup 118 | 3. Define a distribution or packaging method for pre-compiled binary packages 119 | 4. Define or aide package retrieval and extraction 120 | 5. Define or aide source-package building 121 | 122 | 123 | ## The File Format ## {#into.file-format} 124 | 125 | `libman` specifies three classes of files: 126 | 127 | - *The Index* - Only one *Index* file will be used at a time when resolving 128 | package requirements. This file describes a direct mapping between a package 129 | *name* and the path to the corresponding... 130 | - *Package Manifest* - This file simply describes some general attributes about 131 | how the package's libraries needs to be imported. It does not contain much 132 | in the way of package metadata, as this file is only relevant to build 133 | systems. The most important is this files list of `Library` fields, each of which name the path to a... 134 | - *Library Manifest* - Where the real meat of the format resides. A single 135 | *Library* manifest describes exactly one "importable" library. The library 136 | may or may not even have a linkable (e.g., a "header-only" library). 137 | 138 | See the respective sections on [[#file.syntax|The Manifest Syntax]], and 139 | the specifics on [[#file.index|Index Files]], [[#file.package|Package Files]], 140 | and [[#file.library|Library Files]]. 141 | 142 | 143 | # The File Format # {#file} 144 | 145 | ## Base Syntax ## {#file.syntax} 146 | 147 | All libman files are encoded in an extremely simple key-value plaintext format, 148 | which is easy to read and write for both human and machine alike. Files are 149 | encoded using UTF-8. 150 | 151 | The syntax of the file is very simple: 152 | 153 | ```yaml 154 | # This is a comment 155 | Some-Key: Some-Value 156 | ``` 157 | 158 | Keys and values in the file each appear on a different line, with the key and 159 | value being separated by a : (colon followed by a space 160 | character). Only a single space character after the colon is required. Trailing 161 | or leading whitespace from the keys and values is ignored. If a colon is 162 | followed by an immediate line-ending, end-of-file, or the right-hand of the 163 | key-value separator is only whitespace, the value for the associated key is an 164 | empty string. 165 | 166 | The key and value together form a **field**. 167 | 168 | Note: A colon is allowed to be a character in a key (but cannot be the final 169 | character). 170 | 171 | Note: As a general rule, `libman` uses the hyphen `-` as a word separator in 172 | keys, with each word being capitalized. This matches the form of headers from 173 | HTTP and SMTP. 174 | 175 | Advisement: Unlike HTTP, `libman` keys are case-sensitive! 176 | 177 | A field with a certain key might appear multiple times in the file. The 178 | semantics thereof depend on the semantics of the field and file. In general, it 179 | is meant to represent "appending" to the list of the corresponding key. 180 | 181 | Each file in `libman` defines a set of acceptable fields. The appearance of 182 | unspecified fields is not allowed, and should be met with a user-visible warning 183 | (but not an error). There is an exception for keys beginning with `X-`, which 184 | are reserved for tool-specific extensions. The presence of an unrecognized key 185 | beginning with `X-` is not required to produce a warning. 186 | 187 | Lines in which the first non-whitespace character is a `#` should be ignored. 188 | 189 | “Trailing comments” are not supported. A `#` appearing in a key or value must 190 | be considered a part of that key or value. 191 | 192 | Empty or only-whitespace lines are ignored. 193 | 194 | A line-ending is not required at the end of the file. 195 | 196 | Note: Readers are expected to accept a single line feed `\n` as a valid 197 | line-ending. Because trailing whitespace is stripped, a CR-LF `\r\n` is 198 | incidentally a valid line-ending and should result in an identical parse. 199 | 200 | 201 | ## Index Files ## {#file.index} 202 | 203 | Index files specify the names of available packages and the path to a 204 | [[#file.package|Package File]] that can be used to consume them. 205 | 206 | The index file should use the `.lmi` extension. 207 | 208 | 209 | ### Fields ### {#file.index.fields} 210 | 211 | #### `Type` #### {#file.index.fields.type} 212 | 213 | The `Type` field must be specified *exactly once*, and should have the literal 214 | value `Index`. 215 | 216 | ```yaml 217 | Type: Index 218 | ``` 219 | 220 | 221 | #### `Package` #### {#file.index.fields.package} 222 | 223 | The `Package` field appears any number of times in a file, and specifies a 224 | package *name* and a *path* to a [[#file.package|Package File]] on disk. If 225 | a relative path, the path resolves relative to the directory containing the 226 | index file. 227 | 228 | The name and path are separated by a semicolon `;`. Extraneous whitespace 229 | is stripped 230 | 231 | ```yaml 232 | Package: Boost; /path/to/boost.lmp 233 | Package: Qt; /path-to/qt.lmp 234 | Package: POCO; /path-to-poco.lmp 235 | Package: SomethingElse; relative-path/anything.lmp 236 | ``` 237 | 238 | The appearance of two `Package` fields with the same package name is not 239 | allowed and consumers should produce an **error** upon encountering it. 240 | 241 | 242 | ### Example ### {#file.index.example} 243 | 244 |
245 | A simple Index file with a few packages 246 | 247 | ```yaml 248 | # This is an index file 249 | Type: Index 250 | # Some Packages 251 | Package: Boost; /path/to/boost.lmp 252 | Package: Qt; /path-to/qt.lmp 253 | Package: POCO; /path-to-poco.lmp 254 | Package: SomethingElse; relative-path/anything.lmp 255 | ``` 256 |
257 | 258 | 259 | ## Package Files ## {#file.package} 260 | 261 | Package files are found via [[#file.index|Index Files]], and they specify some 262 | number of [[#file.library|Library Files]] to import. 263 | 264 | Package files should use the `.lmp` extension. 265 | 266 | 267 | ### Fields ### {#file.package.fields} 268 | 269 | #### `Type` #### {#file.package.fields.type} 270 | 271 | The `Type` field must be specified *exactly once*, and should have the literal 272 | value `Package`. 273 | 274 | ```yaml 275 | Type: Package 276 | ``` 277 | 278 | 279 | #### `Name` #### {#file.package.fields.name} 280 | 281 | The `Name` field in a package file should be the name of the package, and 282 | should match the name of the package present in the index that points to the 283 | file defining the package. If `Name` is not present or not equal to the name 284 | provided in the index, consumers are not required to generate a warning. It’s 285 | purpose is for the querying of individual package files and for human 286 | consumption. 287 | 288 | ```yaml 289 | Name: Boost 290 | ``` 291 | 292 | 293 | #### `Namespace` #### {#file.package.fields.namespace} 294 | 295 | The `Namespace` field in a package file must appear *exactly once*. It is 296 | not required to correspond to any C++ `namespace`, and is purely for the 297 | namespaces of the import information for consuming tools. For example, CMake 298 | may prepend the `Namespace` and two colons `::` to the name of imported targets 299 | generated from the `libman` manifests. 300 | 301 | ```yaml 302 | Namespace: Qt5 303 | ``` 304 | 305 | Note: The `Namespace` is not required to be unique between packages. Multiple 306 | packages may declare themselves to share a `Namespace`, such as modularized 307 | Boost packages. 308 | 309 | 310 | #### `Requires` #### {#file.package.fields.requires} 311 | 312 | The `Requires` field may appear multiple times, each time specifying the name 313 | of a package which is required in order to use the requiring package. 314 | 315 | When a consumer encounters a `Requires` field, they should use the 316 | [[#file.index|index file]] to find the package specified by the given name. If 317 | no such package is listed in the index, the consumer should generate an error. 318 | 319 | ```yaml 320 | Requires: Boost.Filesystem 321 | Requires: Boost.Coroutine2 322 | Requires: fmtlib 323 | ``` 324 | 325 | Note: The presence of `Requires` **does not** create any usage requirements on 326 | the libraries of the package. It is up to the individual libraries of the 327 | requiring package to explicitly call out their usage of libraries from other 328 | packages via their [[#file.library.fields.uses]] field. This field is purely to 329 | ensure that the definitions from the other package are imported before the 330 | library files are processed. 331 | 332 | 333 | #### `Library` #### {#file.package.fields.library} 334 | 335 | The `Library` field specifies the path to a [[#file.library|library file]]. Each 336 | appearance of the `Library` field specifies another library which should be 337 | considered as part of the package. 338 | 339 | ```yaml 340 | Library: filesystem.lml 341 | Library: system.lml 342 | Library: coroutine2.lml 343 | ``` 344 | 345 | If a relative path, the file path should be resolved relative to the directory 346 | of the package file. 347 | 348 | Note: The filename of a `Library` field is not significant other than in 349 | locating the library file to import. 350 | 351 | 352 | ### Example ### {#file.package.example} 353 | 354 |
355 | A Qt5 example: 356 | 357 | ```yaml 358 | # A merged Qt5 359 | Type: Package 360 | Name: Qt5 361 | Namespace: Qt5 362 | 363 | # Some things we might require 364 | Requires: OpenSSL 365 | Requires: Xcb 366 | 367 | # Qt libraries 368 | Library: Core.lml 369 | Library: Widgets.lml 370 | Library: Gui.lml 371 | Library: Network.lml 372 | Library: Quick.lml 373 | # ... (Qt has many libraries) 374 | ``` 375 |
376 | 377 | 378 | ## Library Files ## {#file.library} 379 | 380 | Library files are found via [[#file.package|Package Files]], and each one 381 | specifies exactly one "library" with a set of usage requirements. 382 | 383 | Library files should use the `.lml` extension. 384 | 385 | 386 | ### Fields ### {#file.library.fields} 387 | 388 | #### `Type` #### {#file.library.fields.type} 389 | 390 | The `Type` field must be specified *exactly once*, and should have the literal 391 | value `Library`. 392 | 393 | ```yaml 394 | Type: Library 395 | ``` 396 | 397 | 398 | #### `Name` #### {#file.library.fields.name} 399 | 400 | The `Name` field must appear exactly once. Consumers should qualify this name 401 | with the containing package’s [[#file.package.fields.namespace|Namespace]] 402 | field to form the identifier for the library. 403 | 404 | ```yaml 405 | Name: Boost 406 | ``` 407 | 408 | 409 | #### `Path` #### {#file.library.fields.path} 410 | 411 | For libraries which provide a linkable, the `Path` field specifies the path to a 412 | file which should be linked into executable binaries. 413 | 414 | This field may be omitted for libraries which do not have a linkable (e.g. 415 | “header-only” libraries). 416 | 417 | ```yaml 418 | Path: lib/libboost_system-mt-d.a 419 | ``` 420 | 421 | 422 | #### `Include-Path` #### {#file.library.fields.include-path} 423 | 424 | Specifies a directory path in which the library’s headers can be found. Targets 425 | which use this library should have the named directory appended to their header 426 | search path. (e.g. using the -I or -isystem flag in GCC). 427 | 428 | This field may appear any number of times. Each appearance will specify an 429 | additional search directory. 430 | 431 | Relative paths should resolve relative to the directory containing the library 432 | file. 433 | 434 | ```yaml 435 | Include-Path: include/ 436 | Include-Path: src/ 437 | ``` 438 | 439 | 440 | #### `Preprocessor-Define` #### {#file.library.fields.preprocessor-define} 441 | 442 | Sets a preprocessor definition that is required to use the library. 443 | 444 | Note: This should not be seen as an endorsement of this design! 445 | 446 | Should be either a legal C identifier, or a C identifier and substitution value 447 | separated with an `=`. (The syntax used by MSVC and GNU-style compiler command 448 | lines). 449 | 450 | ```yaml 451 | Preprocessor-Define: SOME_LIBRARY 452 | Preprocessor-Define: SOME_LIBRARY_VERSION=339 453 | ``` 454 | 455 | 456 | #### `Uses` #### {#file.library.fields.uses} 457 | 458 | Specify a *transitive requirement* for using the library. This must be of the 459 | format `/`, where `` is the string used in the 460 | [[#file.package.fields.namespace|Namespace]] field of the package which defines 461 | ``, and `` is the [[#file.library.fields.name|Name]] field of 462 | the library which we intend to use transitively. 463 | 464 | ```yaml 465 | Uses: Boost/coroutine2 466 | Uses: Boost/system 467 | ``` 468 | 469 | Build systems should use the `Uses` field to apply transitive imported library 470 | target usage requirements. “Using” targets should transitively “use” the 471 | libraries named by this field. 472 | 473 | 474 | #### `Special-Uses` #### {#file.library.fields.special-uses} 475 | 476 | - See: [[#special-reqs]] 477 | 478 | Specifies *Special Requirements* for the library. 479 | 480 | 481 | ### Example ### {#file.library.example} 482 | 483 |
484 | A Catch2 base library, only declaring a directory that needs to be included. It 485 | has no `Path` attribute, and therefore acts as a "header-only" library. 486 | 487 | ```yaml 488 | Type: Library 489 | Name: Catch2 490 | Include-Path: include/ 491 | ``` 492 | 493 | A library that builds upon the main Catch header-only library to provide a 494 | pre-compiled `main()` function, a common use-case with Catch 495 | 496 | ```yaml 497 | Type: Library 498 | # The name is "main" 499 | Name: main 500 | # The static library file to link to 501 | Path: lib/catch_main.a 502 | # We build upon the Catch2/Catch2 sibling library. 503 | Uses: Catch2/Catch2 504 | ``` 505 |
506 | 507 | 508 |
509 | A more concrete example of what a few Boost library files might look like 510 | 511 | ```yaml 512 | # The base "headers" library for Boost 513 | Type: Library 514 | Name: boost 515 | Include-Path: include/ 516 | ``` 517 | 518 | ```yaml 519 | # Boost.System 520 | Type: Library 521 | Name: system 522 | Uses: Boost/boost 523 | Path: lib/libboost_system.a 524 | ``` 525 | 526 | ```yaml 527 | # Boost.Asio 528 | Type: Library 529 | Name: asio 530 | # Note: Does not depend on Boost/boost nor Boost/context directly. It inherits 531 | # those transitively. 532 | Uses: Boost/system 533 | Uses: Boost/coroutine 534 | ``` 535 | 536 | ```yaml 537 | # Boost.Beast 538 | Type: Library 539 | Name: beast 540 | Uses: Boost/asio 541 | ``` 542 |
543 | 544 | 545 | # Usage and Semantics # {#sema} 546 | 547 | Although the `libman` files can be created and consume by human eye and hand, a typical use case will see the `libman` files generated by a PDM and consumed by a build system. 548 | 549 | ## The Index ## {#sema.index} 550 | 551 | The purpose of the *Index* is to define name-based package lookup for a build 552 | system. 553 | 554 | A PDM should generate an index where each package within the index has a 555 | uniform ABI. That is: An executable binary should be able to incorporate all 556 | compiled code from every library from every package within and index and 557 | produce no ODR nor ABI violations. A package may only appear once in an index. 558 | 559 | Note: To service the case of build systems which support building multiple 560 | "build types" simultaneously, a PDM and build system may coordinate multiple 561 | indices, with one for each "build type" that the build system wishes to consume. 562 | 563 | ### The `libman` Tree ### {#sema.index.tree} 564 | 565 | Given a single index file, one can generate a single `libman` "tree" with the 566 | index at the root, packages at the next level, and libraries at the bottom level. 567 | 568 | ```text 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | ``` 583 | 584 | ### Uniqueness of Packages and Libraries ### {#sema.index.unique} 585 | 586 | Each package must be unique in a tree. Each library will be unique given its 587 | qualified name of the form `/` (Where `` is 588 | declared by the [[#file.package.fields.namespace]] field of the package from 589 | which it was referred). The library [[#file.library.fields.name]] field might 590 | *not* be unique. Disambiguating similarly named libraries is the purpose of the 591 | package's `Namespace`, as it is unlikely (and unsupported) for a single package 592 | to declare more than one library with the name `Name`. 593 | 594 | Note: Although `libman` uses the qualified form `/`, other 595 | tools may use their own format for the qualification. For example, CMake might 596 | use `::` to refer to the imported target or Scons may use 597 | `__`. It is up to the individual tool to select, implement, 598 | and document the appropriate qualification format for their users. 599 | 600 | 601 | ### Index Location ### {#sema.index.location} 602 | 603 | When a build system wishes to use an index file, it should offer the user a 604 | way to specify the location explicitly. If no location is provided by a user, 605 | it should prefer the following: 606 | 607 | - A file named ``INDEX.lmi`` within the root of the project directory. 608 | - A file named ``INDEX.lmi`` within the root of the build directory for the 609 | project. 610 | - Optionally, a file named ``INDEX-.lmi`` within the root of the 611 | project directory. 612 | - Optionally, a file named ``INDEX-.lmi`` within the root of the build 613 | - directory for the project. 614 | 615 | In the above, ```` is a string specifying a "build type" for the build 616 | system. This is intended to facilitate build systems which are "multi-conf" 617 | aware. 618 | 619 | 620 | ## Packages ## {#sema.package} 621 | 622 | - See: [[#file.package]] 623 | 624 | Packages are defined in `libman` to be a collection of some number of libraries. 625 | They contain a [[#file.package.fields.namespace]] field to qualify their 626 | libraries, and may declare the reliance on the presence of other libraries 627 | using [[#file.package.fields.requires]]. 628 | 629 | Note: The `Requires` field is *not for dependency managers*: It's for *build 630 | systems* to know what other packages need to be imported when importing a 631 | package. Indeed, all of the information in `libman` is for *build systems* to 632 | consume, not dependency managers. 633 | 634 | 635 | ### Where Does `Namespace` Come From? ### {#sema.package.namespace} 636 | 637 | - See: [[#file.package.fields.namespace]] 638 | 639 | In short: It comes from the upstream developer. 640 | 641 | The `Namespace` should originate from the package itself, and be specified by 642 | the maintainer, not something generated by the dependency management system, nor 643 | by a third-party packager. 644 | 645 | Placing this responsibility on the upstream developer ensures that all package 646 | maintainers end up with the same `Namespace` in their `libman` files, ensuring 647 | that the [[#file.library.fields.uses]] field from libraries of other packages 648 | are able to successfully resolve. 649 | 650 | Note: In the case that the package's upstream developer cannot be contacted or 651 | does not voice an opinion, the appropriate `Namespace` should be chosen by the 652 | package maintainer carefully to create minimal confusion for package users. 653 | Package maintainers for different PDMs are encouraged to collaborate and 654 | consolidate on a single `Namespace`. 655 | 656 | 657 | ### The `Requires` Graph ### {#sema.package.requires} 658 | 659 | - See: [[#file.package.fields.requires]] 660 | 661 | A `Requires` field of a package may only specify packages which are defined in 662 | the current `libman` tree (generated from the current index). Build systems 663 | must resolve the `Requires` recursively. Build systems *must* process the 664 | packages named by the `Requires` field before processing the package which 665 | namespace the requirement. The result will be a directed acyclic graph of the 666 | package dependencies. 667 | 668 | If the `Requires` field names a package not contained in the current tree, 669 | build systems must generate an error. A well-formed index and `libman` tree 670 | should never encounter this issue, and the onus is on PDMs to generate a 671 | conforming index file. Regular user action should never create a situation 672 | where a `Requires` field is unsatisfied by the index from which the requiring 673 | package was found. 674 | 675 | `Requires` may not form a cyclic dependency graph. 676 | 677 | 678 | ## Libraries ## {#sema.library} 679 | 680 | - See: [[#file.library]] 681 | 682 | *Libraries* are the main consumable for development package managers. In C++ we 683 | define a *library* as a set of interconnected translation units and/or 684 | `#include`-able code that provides some pre-packaged functionality that we wish 685 | to incorporate into our own project. 686 | 687 | Consuming a library requires (1) being capable of using the preprocessor 688 | `#include` directive to incorporate the headers from the library and (2) being 689 | able to resolve entities of external linkage which are defined within the 690 | headers for that library. (Some libraries may have no entities with external 691 | linkage.) 692 | 693 | 694 | ### Canonical `#include` Directives ### {#sema.library.canon-include} 695 | 696 | The characters within the `<>` of `#include <...>` are of incredible importance. 697 | `libman` encourages libraries to define a single "canonical" `#include` 698 | directive for their files. A user *must not* have to *guess* which include 699 | directive is correct. To support this, libraries may declare the directory in 700 | which their "canonical include directives" may be resolved via the 701 | [[#file.library.fields.include-path]] field. 702 | 703 | 704 | ### Recommendation: Avoid Header Mixing ### {#sema.library.header-mixing} 705 | 706 | **Headers for libraries should avoid intermixing with the headers of other 707 | libraries, even of other libraries within the same package.** 708 | 709 | Upon declaring their intent to "use" a library, a user should be able to 710 | `#include` the headers of that library using the "canonical include directives" 711 | for that library. 712 | 713 | If a user *does not* declare their usage of a library (either directly or 714 | indirectly from transitive [[#file.library.fields.uses]]), they *should not be 715 | able* to `#include` headers from that library. 716 | 717 | Mixing headers between libraries in a single `Include-Path` allows the user to 718 | make use of an entity of external linkage from a library without declaring 719 | their "usage" of that library, and therefore causes those entities to fail 720 | resolution at the link stage because the build system is unaware of their 721 | intent to *use* that library. 722 | 723 | "Using" a library should cause the headers to be visible, but will also enforce 724 | that the external linkage entities are resolved. 725 | 726 | Note: While the admonition to "avoid header mixing" is partially aimed at 727 | library developers, this admonition can apply equally to dependency managers 728 | who have the duty of placing the headers files in the filesystem at install 729 | time. 730 | 731 | Note: **Yes**, this is a break from the FHS's `/usr/include` and 732 | `/usr/local/include` directories. These have been very convenient in the past, 733 | but have proven very problematic for the case of unprivileged user development. 734 | 735 | 736 | ### Transitive Usage with `Uses` ### {#sema.library.uses} 737 | 738 | - See: [[#file.library.fields.uses]] 739 | - See: [[#sema.package.namespace]] 740 | 741 | The `Uses` field is meant to represent transitive requirements. Libraries which 742 | build upon other libraries should declare this fact via `Uses`. 743 | 744 | The syntax of a `Uses` entry is `/`, where `` is 745 | the [[#file.package.fields.namespace]] field of the [[#file.package|package]] 746 | which owns the library, and `` is simply the 747 | [[#file.library.fields.name]] field from the library. 748 | 749 | Build systems should translate the `Uses` field to an appropriate transitive 750 | dependency in the build system's own representation. The exact spelling and 751 | implementation of this dependency is not specified here, but must meet the 752 | requirement of *transitivity*: If `A` uses `B`, and `foo` uses `A` *directly*, 753 | then `foo` should behave *as if* it uses `B`. `foo` is said to use `B` 754 | *indirectly*. 755 | 756 | 757 | ## Special Requirements ## {#special-reqs} 758 | 759 | - See: [[#file.library.fields.special-uses]] 760 | 761 | *Special Requirements* are [[#intro.usage-requirements]] that do not correspond 762 | to a library or package provided by the PDM. The semantics of a Special 763 | Requirement are platform-specific, but their intended semantics are outlined 764 | here. Special requirements *may* be namespace with `/`, but 765 | libman reserves all unqualified names. Platforms and build systems may define 766 | additional Special Requirements using qualified names. 767 | 768 | 769 | ### `Threading` ### {#special-reqs.threading} 770 | 771 | Enables threading support. Some platforms require compile and/or link options 772 | to enable support for threading in the compiled binary. For example, GCC 773 | requires `-pthread` as a compile and link option for `std::thread` and several 774 | other threading primitives to operate correctly. 775 | 776 | 777 | ### `Filesystem` ### {#special-reqs.filesystem} 778 | 779 | Enables support for C++17's filesystem library. Some platforms require an 780 | additional support library to be linked in order to make use of the facilities 781 | of `std::filesystem`. 782 | 783 | 784 | ### `DynamicLinker` ### {#special-reqs.dl} 785 | 786 | Enables support for runtime dynamic linking, for example using `dlopen()`. 787 | 788 | 789 | ### `PosixRealtime` ### {#special-reqs.realtime} 790 | 791 | Enable support for POSIX realtime extensions. For example, required for shared 792 | memory functions on some platforms. 793 | 794 | 795 | ### `Math` ### {#special-reqs.math} 796 | 797 | Enable support for ``. Some platforms provide the definitions of the 798 | math functions in a separate library that is not linked by default. 799 | 800 | 801 | ### `Sockets` ### {#special-reqs.sockets} 802 | 803 | Enable support for socket programming. For example, Windows requires linking in 804 | the Winsock libraries in order to make use of the Windows socket APIs. 805 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build/ -------------------------------------------------------------------------------- /docs/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | find_program(SPHINX_BUILD_EXECUTABLE sphinx-build DOC "Path to sphinx-build, for documentation") 2 | 3 | if(NOT SPHINX_BUILD_EXECUTABLE) 4 | message(STATUS "No sphinx-build, so we will not generate the documentation") 5 | return() 6 | endif() 7 | 8 | get_filename_component(doc_build_dir "${CMAKE_CURRENT_BINARY_DIR}/html" ABSOLUTE) 9 | 10 | file(GLOB_RECURSE doc_files CONFIGURE_DEPENDS "*") 11 | add_custom_command( 12 | OUTPUT "${doc_build_dir}/index.html" 13 | DEPENDS ${doc_files} 14 | COMMAND "${SPHINX_BUILD_EXECUTABLE}" 15 | -b html 16 | -q 17 | -j6 18 | -W 19 | "${CMAKE_CURRENT_SOURCE_DIR}" 20 | "${doc_build_dir}" 21 | COMMENT "Generating documentation" 22 | ) 23 | add_custom_target(lm-docs ALL DEPENDS "${doc_build_dir}/index.html") 24 | -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | /* Keep the sidebare visible in by sticking it when scrolling */ 2 | div.sphinxsidebar { 3 | position: sticky; 4 | top: 0; 5 | max-height: 100vh; 6 | overflow-y: auto; 7 | } 8 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'libman' 23 | copyright = '2018, vector-of-bool' 24 | author = 'vector-of-bool' 25 | 26 | # The short X.Y version 27 | version = '' 28 | # The full version, including alpha/beta/rc tags 29 | release = '' 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # If your documentation needs a minimal Sphinx version, state it here. 35 | # 36 | # needs_sphinx = '1.0' 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | ] 43 | 44 | # Add any paths that contain templates here, relative to this directory. 45 | templates_path = [] 46 | 47 | # The suffix(es) of source filenames. 48 | # You can specify multiple suffix as a list of string: 49 | # 50 | # source_suffix = ['.rst', '.md'] 51 | source_suffix = '.rst' 52 | 53 | # The master toctree document. 54 | master_doc = 'index' 55 | 56 | # The language for content autogenerated by Sphinx. Refer to documentation 57 | # for a list of supported languages. 58 | # 59 | # This is also used if you do content translation via gettext catalogs. 60 | # Usually you set "language" from the command line for these cases. 61 | language = None 62 | 63 | # List of patterns, relative to source directory, that match files and 64 | # directories to ignore when looking for source files. 65 | # This pattern also affects html_static_path and html_extra_path . 66 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 67 | 68 | # The name of the Pygments (syntax highlighting) style to use. 69 | pygments_style = 'sphinx' 70 | 71 | 72 | # -- Options for HTML output ------------------------------------------------- 73 | 74 | # The theme to use for HTML and HTML Help pages. See the documentation for 75 | # a list of builtin themes. 76 | # 77 | html_theme = 'nature' 78 | 79 | # Theme options are theme-specific and customize the look and feel of a theme 80 | # further. For a list of options available for each theme, see the 81 | # documentation. 82 | # 83 | # html_theme_options = {} 84 | 85 | # Add any paths that contain custom static files (such as style sheets) here, 86 | # relative to this directory. They are copied after the builtin static files, 87 | # so a file named "default.css" will overwrite the builtin "default.css". 88 | html_static_path = ['_static'] 89 | 90 | # Custom sidebar templates, must be a dictionary that maps document names 91 | # to template names. 92 | # 93 | # The default sidebars (for documents that don't match any pattern) are 94 | # defined by theme itself. Builtin themes are using these templates by 95 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 96 | # 'searchbox.html']``. 97 | # 98 | # html_sidebars = {} 99 | 100 | 101 | # -- Options for HTMLHelp output --------------------------------------------- 102 | 103 | # Output file base name for HTML help builder. 104 | htmlhelp_basename = 'libmandoc' 105 | 106 | 107 | # -- Options for LaTeX output ------------------------------------------------ 108 | 109 | latex_elements = { 110 | # The paper size ('letterpaper' or 'a4paper'). 111 | # 112 | # 'papersize': 'letterpaper', 113 | 114 | # The font size ('10pt', '11pt' or '12pt'). 115 | # 116 | # 'pointsize': '10pt', 117 | 118 | # Additional stuff for the LaTeX preamble. 119 | # 120 | # 'preamble': '', 121 | 122 | # Latex figure (float) alignment 123 | # 124 | # 'figure_align': 'htbp', 125 | } 126 | 127 | # Grouping the document tree into LaTeX files. List of tuples 128 | # (source start file, target name, title, 129 | # author, documentclass [howto, manual, or own class]). 130 | latex_documents = [ 131 | (master_doc, 'libman.tex', 'libman Documentation', 132 | 'vector-of-bool', 'manual'), 133 | ] 134 | 135 | 136 | # -- Options for manual page output ------------------------------------------ 137 | 138 | # One entry per manual page. List of tuples 139 | # (source start file, name, description, authors, manual section). 140 | man_pages = [ 141 | (master_doc, 'libman', 'libman Documentation', 142 | [author], 1) 143 | ] 144 | 145 | 146 | # -- Options for Texinfo output ---------------------------------------------- 147 | 148 | # Grouping the document tree into Texinfo files. List of tuples 149 | # (source start file, target name, title, author, 150 | # dir menu entry, description, category) 151 | texinfo_documents = [ 152 | (master_doc, 'libman', 'libman Documentation', 153 | author, 'libman', 'One line description of project.', 154 | 'Miscellaneous'), 155 | ] 156 | 157 | def setup(app): 158 | app.add_stylesheet('custom.css') 159 | -------------------------------------------------------------------------------- /docs/files.rst: -------------------------------------------------------------------------------- 1 | .. _lm.files: 2 | 3 | *libman* Files 4 | ############## 5 | 6 | *libman* defines a few files, all using a simple :ref:`key-value plaintext file 7 | format `: 8 | 9 | - :ref:`The Index ` - Defines the mapping between package names and 10 | the path to the package files on disk. 11 | - :ref:`Package Files ` - Defines importable *packages*. Each 12 | package may itself define one or more *libraries*. 13 | - :ref:`Library Files ` - Each defining a single importable library. 14 | 15 | None of the files may be consumed individually since the files can (and will) 16 | refer to each-other. See the individual file documentation pages for details. 17 | 18 | Table of Contents 19 | ----------------- 20 | 21 | .. toctree:: 22 | :maxdepth: 2 23 | 24 | format 25 | index-file 26 | package-file 27 | library-file 28 | -------------------------------------------------------------------------------- /docs/format.rst: -------------------------------------------------------------------------------- 1 | .. _lm.format: 2 | 3 | Base File Format 4 | ################ 5 | 6 | All *libman* files are encoded in an extremely simple key-value plaintext 7 | format, which is easy to read and write for human and machine alike. 8 | 9 | Syntax 10 | ****** 11 | 12 | The syntax of the file is very simple: 13 | 14 | .. code-block:: yaml 15 | 16 | # This is a comment 17 | Some-Key: Some-Value 18 | 19 | Keys and values in the file each appear on a different line, with the key and 20 | value being separated by a ": " (colon followed by a space). Only a single space 21 | after the colon is required. Trailing or leading whitespace from the keys and 22 | values is ignored. If a colon is followed by an immediate line-ending, 23 | end-of-file, or the right-hand of the key-value separator is only whitespace, 24 | the value for the associated key is an empty string. Note that a colon *is 25 | allowed* to be a character in a key (but cannot be the final character). 26 | 27 | The key and value together form a "field." 28 | 29 | **KEYS AND VALUES ARE CASE-SENSITIVE.** 30 | 31 | A field *might* appear multiple times in the file. The semantics thereof depend 32 | on the semantics of the field. 33 | 34 | Each file in *libman* defines a set of acceptable fields. The appearance of 35 | unspecified fields is not allowed, and should be met with a user-visible warning 36 | (but *not an error*). There is an exception for keys beginning with `X-`, which 37 | are reserved for tool-specific extensions. The presence of an unrecognized key 38 | beginning with `X-` is not required to produce a warning. 39 | 40 | Lines in which the first non-whitespace character is a ``#`` should be ignored. 41 | 42 | "Trailing comments" are not supported. A ``#`` appearing in a key or value 43 | must be considered a part of that key or value. 44 | 45 | Empty or only-whitespace lines are ignored. 46 | 47 | Readers are expected to accept a single line feed ``\n`` as a valid line-ending. 48 | Because trailing whitespace is stripped, a CR-LF ``\r\n`` is *incidentally* a 49 | valid line-ending and should result in an identical parse. 50 | 51 | A line-ending is not required at the end of the file. 52 | -------------------------------------------------------------------------------- /docs/index-file.rst: -------------------------------------------------------------------------------- 1 | .. _lm.index: 2 | 3 | The Index File 4 | ############## 5 | 6 | .. note:: 7 | Read :ref:`lm.format` first. 8 | 9 | The purpose of the *index* file is to specify the mapping between package names 10 | and the location of their respective :ref:`package files ` on disk. 11 | 12 | The file extension of an index file is ``.lmi``. 13 | 14 | 15 | Fields 16 | ****** 17 | 18 | 19 | ``Type`` 20 | ======== 21 | 22 | The ``Type`` field in an index file *must* be ``Index``, and must appear 23 | *exactly once*:: 24 | 25 | Type: Index 26 | 27 | 28 | ``Package`` 29 | =========== 30 | 31 | The ``Package`` field appears any number of times in a file, and specifies a 32 | package *name* and a *path* to a :ref:`package file ` on disk. If 33 | a relative path, the path resolves relative to the directory containing the 34 | index file. 35 | 36 | The name and path are separated by a semicolon ``;``. Extraneous whitespace 37 | is stripped:: 38 | 39 | Package: Boost; /path/to/boost.lmp 40 | Package: Qt; /path-to/qt.lmp 41 | Package: POCO; /path-to-poco.lmp 42 | Package: SomethingElse; relative-path/anything.lmp 43 | 44 | The appearance of two ``Package`` fields with the same package name is not 45 | allowed and consumers should produce an **error** upon encountering it. 46 | 47 | 48 | Example 49 | ******* 50 | 51 | .. code-block:: yaml 52 | 53 | # This is an index file 54 | Type: Index 55 | 56 | # Some Packages 57 | Package: Boost; /path/to/boost.lmp 58 | Package: Qt; /path-to/qt.lmp 59 | Package: POCO; /path-to-poco.lmp 60 | Package: SomethingElse; relative-path/anything.lmp 61 | 62 | 63 | Rationale and Intended Usage 64 | **************************** 65 | 66 | A index file is intended to be produced by a tool automatically to provide a 67 | consumer with a complete, consistent, and coherent view of a package hierarchy. 68 | 69 | **Note the lack of version or ABI information.** It is up to the generating 70 | tool to ensure that the packages listed therein have consistent ABIs and 71 | version compatibilities. It is not the concern of the consumer to perform ABI 72 | and version resolution. 73 | 74 | .. note:: 75 | 76 | By implication, a single binary should be able to simultaneously link to 77 | *every library and every package* reachable via a single index file without 78 | encountering any ABI or version conflicts. 79 | 80 | However, *libman* specifically facilitates pick-and-choose linking on a 81 | per-target basis. Build systems should only use the *minimum* required 82 | based on what is explicitly requested by the user for each target. 83 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | libman - The Library Manifest Format 2 | ==================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | intro 9 | overview 10 | files 11 | format 12 | 13 | Indices and tables 14 | ================== 15 | 16 | * :ref:`genindex` 17 | * :ref:`search` 18 | 19 | .. * :ref:`modindex` 20 | 21 | -------------------------------------------------------------------------------- /docs/intro.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ############ 3 | 4 | This page will give you an introduction to ``libman``. What is it? What problems 5 | does it solve? etc. 6 | 7 | 8 | Background 9 | ********** 10 | 11 | C++ is late to the game in terms of a unified and cross-platform solution for 12 | easily distributing native libraries. 13 | 14 | There are "native" "system" package managers and package formats, such as 15 | `dpkg`/`apt`, `yum`/`rpm`, `pacman`, and MSI that address a core problem of 16 | managing the global state of a given machine, and these packaging formats often 17 | deal with "native" compiled binaries. These solutions are *not* cross-platform, 18 | and *even if they were*, **none** of them are appropriate for the problem at 19 | hand. They are not focused on development and compilation within the C++ 20 | ecosystem. While other languages and ecosystems have tools such as ``pip``, 21 | ``npm``, ``cargo``, ``mix``, and even ``cpan``, C++ has gone a long time with 22 | no widely-accepted solution. 23 | 24 | Why is this? 25 | 26 | There have been many attempts, and there are presently several competing 27 | alternatives for solving the package and dependency management problem. Running 28 | in parallel, and solving a (somewhat) orthogonal problem, are several competing 29 | build systems. CMake, Meson, Waf, b2, and countless others have been pining for 30 | the love and attention of the C++ community for years. All of them wildly 31 | incompatible in terms of their package consumption formats. 32 | 33 | This situation presents a unique problem. With lack of a "reference 34 | implementation" of C++, and no singular and completely universal build tool and 35 | format, we have an N:M problem of N package and dependency managers attempting 36 | to work with M build systems. 37 | 38 | 39 | Some Examples 40 | ************* 41 | 42 | As an example, we will consider the way in which two popular package and 43 | dependency managers work with CMake, a widely used C++ build system. 44 | 45 | 46 | Using Conan with CMake 47 | ====================== 48 | 49 | Consider the following simple CMake project: 50 | 51 | .. code-block:: cmake 52 | 53 | cmake_minimum_required(VERSION 3.12) 54 | project(SomeProject VERSION 1.0.0) 55 | 56 | add_executable(my-program main.cpp) 57 | 58 | # Link to fmtlib ?? 59 | 60 | We want to use `fmtlib `_ in our program. Let's write a 61 | ``conanfile.txt`` to do this: 62 | 63 | .. code-block:: ini 64 | 65 | [requires] 66 | fmt/5.2.0@bincrafters/stable 67 | 68 | [generators] 69 | cmake 70 | 71 | Now we need to install our dependencies with ``conan install .``. This will 72 | place a ``conanbuildinfo.cmake`` file in the directory in which we run the 73 | ``conan install``. We must modify our CMake project to import this file 74 | appropriately, and we can now link against ``fmtlib``: 75 | 76 | .. code-block:: cmake 77 | 78 | cmake_minimum_required(VERSION 3.12) 79 | project(SomeProject VERSION 1.0.0) 80 | 81 | include(conanbuildinfo.cmake) 82 | conan_basic_setup(TARGETS) 83 | 84 | add_executable(my-program main.cpp) 85 | 86 | target_link_libraries(my-program PRIVATE CONAN_PKGS::fmt) 87 | 88 | Mmm... Despite the goal of keeping ourselves agnostic of a particular PDM, our 89 | build system now explicitly requests Conan, and only works with Conan. The 90 | correct thing is to use *conditional Conan support:* 91 | 92 | .. code-block:: cmake 93 | 94 | cmake_minimum_required(VERSION 3.12) 95 | project(SomeProject VERSION 1.0.0) 96 | 97 | if(EXISTS conanbuildinfo.cmake) 98 | include(conanbuildinfo.cmake) 99 | conan_basic_setup(TARGETS) 100 | endif() 101 | 102 | add_executable(my-program main.cpp) 103 | 104 | if(TARGET CONAN_PKGS::fmt) 105 | target_link_libraries(my-program PRIVATE CONAN_PKGS::fmt) 106 | else() 107 | # ... ? 108 | endif() 109 | 110 | This *looks* like it works, but we've still got the problem of not having access 111 | to `libfmt` when Conan isn't in use. For any alternative PDM we'd need to encode 112 | additional logic to behave differently depending on what environment we are in. 113 | 114 | .. note:: 115 | This author is aware that *some* Conan packages will work with 116 | ``find_package()``, **but** this isn't universally available for all Conan 117 | packages, and it presents its own set of problems that make it insufficient. 118 | 119 | 120 | Using ``vcpkg`` with CMake 121 | ========================== 122 | 123 | Another popular PDM is ``vcpkg``, a tool from Microsoft that takes a different 124 | approach to packaging. It won't be detailed in full here, as it is out-of-scope. 125 | 126 | Here's our same CMake project, but using ``vcpkg`` to manage its dependencies: 127 | 128 | .. code-block:: cmake 129 | 130 | cmake_minimum_required(VERSION 3.12) 131 | project(SomeProject VERSION 1.0.0) 132 | 133 | find_package(fmt REQUIRED) 134 | 135 | add_executable(my-program main.cpp) 136 | 137 | target_link_libraries(my-program PRIVATE fmt::fmt) 138 | 139 | You'll notice the distinct lack of "vcpkg" being mentioned anywhere. This is 140 | because vcpkg takes the idea of build systems and dependency management to its 141 | fullest. To use vcpkg, you must invoke the tool outside of your build: 142 | 143 | :: 144 | 145 | $ vcpkg install fmt 146 | 147 | And then invoke CMake using vcpkg's "toolchain" file: 148 | 149 | :: 150 | 151 | $ cmake -D CMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/toolchain/vcpkg.cmake 152 | 153 | This "toolchain" file is a way that vcpkg will "hook into" your build system the 154 | first time CMake attempts to learn about the present compiler. The important 155 | step (for our project) is that vcpkg modifies ``CMAKE_PREFIX_PATH`` such that 156 | ``find_package()`` will search in a vcpkg-generated directory where an 157 | ``fmtConfig.cmake`` file can be found. (The exact details of how this is done 158 | and what a "packageConfig.cmake" file are is outside the scope of this 159 | document). 160 | 161 | In this way vcpkg is able to tweak the build system to be aware of vcpkg without 162 | the user having to modify their build system. 163 | 164 | It would seem vcpkg is surely superior. Right? 165 | 166 | Not so fast! We still have a problem: ``find_package()`` works great *when it 167 | works at all.* 168 | 169 | ``find_package()`` Finds Problems 170 | ================================= 171 | 172 | Not all packages provide support for CMake's ``packageConfig.cmake`` format, and 173 | even if they did it would still be CMake-specific. Library authors and/or 174 | packagers would be forced to write and maintain these build-system-specific 175 | integration files. No work that goes into writing ``fmtConfig.cmake`` does any 176 | good for any build system besides CMake. 177 | 178 | There are a few more problems with ``find_package()`` that become very prevalent 179 | when we remove the PDM from the picture: 180 | 181 | 1. We search implicit global directories not controlled by a dependency manager 182 | (Not counting system package managers, which *are not developer tools* and 183 | should not be treated as such). 184 | 2. *Even if* a PDM is in use, a missing explicit requirement will cause 185 | ``find_package()`` to fall-through to search the system, when we want to 186 | keep all of our dependencies under the control of the PDM. ``find_package()`` 187 | can successfully find system components when we meant to find PDM-controlled 188 | components, hiding missing requirements and dependency compatibility issues. 189 | 3. ``find_package()`` supports a ``VERSION`` argument, but it is *extremely 190 | poor* in its capability. It is entirely up to the package being found to 191 | respect the version request. It is perfectly valid for a found package to 192 | *completely ignore* our version request. Even if a package *does* honor this 193 | request, it may have different definitions of "compatible" between its 194 | version and the version we request. 195 | 4. If ``find_package()`` finds multiple compatible versions, it will simply pick 196 | the first version that was found during the scan. This can lead to 197 | non-deterministic versioning between builds. 198 | 5. ``find_package()`` has no sense of transitive dependencies. 199 | 6. ``find_package()`` has no sense of ABI compatibility. 200 | 7. ``find_package()`` does nothing to help with transitive versioning issues, 201 | e.g. "dependency diamonds." 202 | 8. ``find_package()`` does not enforce any semantics on the package being 203 | imported. Modern CMake packages will usually expose "imported targets," which 204 | present usage requirements and enforce dependency linking. This is *extremely 205 | helpful*, and is a desirable quality in a build system. Unfortunately, 206 | ``find_package()`` is merely a way to find a CMake script matching a certain 207 | file name, and executing it once it is found. ``find_package()`` essentially 208 | executes *arbitrary code*, and it will hopefully "do the right thing." 209 | 210 | All of the above can be fixed and cobbled together on top of CMake's current 211 | ``packageConfig.cmake`` format (and has! This author has helped build and 212 | maintain such a system for several years.) 213 | 214 | **But none of it matters,** because none of it is portable. CMake *may* be 215 | widely popular *today,* but committing to any specific build system could prove 216 | fatal for a PDM. 217 | 218 | 219 | Usage Requirements 220 | ****************** 221 | 222 | The concept of *usage requirements* originated from Boost's b2 build system, 223 | and has been slowly bleeding into general acceptance via CMake. After years of 224 | experience with CMake, and as it has been developing and maturing its 225 | realization of *usage requirements* and the concept of the "usage interface," 226 | it is clear that it is *the* path forward. As such, ``libman`` is explicitly 227 | designed around this concept. 228 | 229 | What are "usage requirements" (also known as the "link interface" or "usage 230 | interface")? 231 | 232 | When we have a "target" (A thing that one wishes to build), we can say that it 233 | "uses" a library. When we "use" a library, we need to inherit certain attributes 234 | thereof which have a direct effect on the way that the final target will be 235 | built. This includes, but is not limited to: 236 | 237 | - **What header search paths do we need?** This ensures that the consumer target 238 | is able to ``#include`` the files from the library being consumed. 239 | - **What files do we need to include in the link phase?** This ensures that 240 | entities with external linkage declared in the library's headers are available 241 | for symbol resolution during the final link. 242 | - **What link/compile options are required?** In some rare cases, consuming a 243 | library will require that certain options be enabled/disabled on the compile 244 | or link phase. **This is not recommended, but is still very common.** 245 | - **Who else do we need to "use"?** Library composition isn't a new idea, and 246 | it encourages modularity and encapsulation. To ensure that we are able to 247 | consume a library which builds upon another, we need to be sure that we can 248 | *also* use the transitive dependencies. This recurses through the "usage" 249 | directed graph until we have satisfied all the usage requirements for a tree. 250 | 251 | ``libman`` defines a platform-agnostic and build-system-agnostic format for 252 | describing these "usage requirements", including how we can import dependencies 253 | transitively. Any build system which can represent the above concepts can import 254 | ``libman`` files. Any PDM which can represent the above concepts can generate 255 | ``libman`` files. 256 | 257 | Therefore, any ``libman``-capable build system can be used with any 258 | ``libman``-capable package and dependency manager. 259 | -------------------------------------------------------------------------------- /docs/library-file.rst: -------------------------------------------------------------------------------- 1 | .. _lm.library: 2 | 3 | Library Files 4 | ############# 5 | 6 | .. note:: 7 | Read :ref:`lm.format` first. 8 | 9 | A *library* is a reusable unit of code that can be consumed by an external 10 | project. It has some number of directories for headers and linkables. A library 11 | file describes how build systems should integrate a library in their targets. 12 | 13 | The file extension of a library file is ``.lml``. 14 | 15 | 16 | Fields 17 | ****** 18 | 19 | 20 | ``Type`` 21 | ======== 22 | 23 | The ``Type`` field in a library file *must* be ``Library``, and must appear 24 | *exactly once*:: 25 | 26 | Type: Library 27 | 28 | 29 | ``Name`` 30 | ======== 31 | 32 | The ``Name`` field must appear *exactly once*. Consumers should qualify this 33 | name and the containing package's ``Namespace`` field to form the identifier 34 | for the library. 35 | 36 | :: 37 | 38 | Name: system 39 | 40 | 41 | ``Path`` 42 | ======== 43 | 44 | For libraries which provide a linkable, the ``Path`` field specifies the path 45 | to a file which should be linked into executable binaries. This may be a static 46 | or dynamic library. 47 | 48 | This field may be omitted for libraries which do not have a linkable (e.g. 49 | "header-only" libraries). 50 | 51 | :: 52 | 53 | Path: lib/libboost_system-mt-d.a 54 | 55 | 56 | ``Include`` 57 | =========== 58 | 59 | Specifies a directory path in which the library's headers can be found. Targets 60 | which use this library should have the named directory appended to their header 61 | search path. (e.g. using the ``-I`` or ``-isystem`` flag in GCC). 62 | 63 | This field may appear any number of times. Each appearance will specify an 64 | additional search directory. 65 | 66 | Relative paths should resolve relative to the directory containing the library 67 | file. 68 | 69 | :: 70 | 71 | Include: include/ 72 | Include: src/ 73 | 74 | 75 | ``Define`` 76 | ========== 77 | 78 | Sets a preprocessor define that is required to use the library. 79 | 80 | .. note:: 81 | This should not be seen as an endorsement of this design. The author would 82 | prefer that libraries use a "config header" than to require their consumers 83 | to set preprocessor definitions. 84 | 85 | Nevertheless: people do it, so we support it. 86 | 87 | Should be either a legal C identifier, or a C identifier and substitution value 88 | separated with an ``=``. (The syntax used by MSVC and GNU-style compiler command 89 | lines). 90 | 91 | :: 92 | 93 | Define: SOME_LIBRARY 94 | Define: SOME_LIBRARY_VERSION=339 95 | 96 | 97 | ``Uses`` 98 | ======== 99 | 100 | Specify a *transitive requirement* for using the library. This must be of the 101 | format ``/``, where ```` is the string used in 102 | the ``Namespace`` field of the package which defines ````, and 103 | ```` is the ``Name`` field of the library which we intend to use 104 | transitively. 105 | 106 | :: 107 | 108 | Uses: Boost/coroutine2 109 | Uses: Boost/system 110 | 111 | Build systems should use the ``Uses`` field to apply transitive imported 112 | library target usage requirements. "Using" targets should transitively "use" 113 | the libraries named by this field. 114 | 115 | 116 | ``Links`` 117 | ========= 118 | 119 | Specifiy a *transitive linking requirement* for using the library. This is 120 | the same format and intention for ``Uses`` field, but only the transitive usage 121 | requirements related to linking need to be propagated. 122 | 123 | .. note:: 124 | This may not be implementable distinctly from ``Uses`` on some build 125 | systems. In such a case, the behavior should be the same as the ``Uses`` 126 | field. 127 | 128 | :: 129 | 130 | Links: Boost/system 131 | Links: Qt5/Core 132 | 133 | -------------------------------------------------------------------------------- /docs/overview.rst: -------------------------------------------------------------------------------- 1 | A ``libman`` Overview 2 | ##################### 3 | 4 | What is "libman?" It's a shortening of "library manifest." It's a combination of 5 | file format and design specification to bridge the gap between build systems 6 | and package/dependency managers (PDMs). 7 | 8 | Goals 9 | ***** 10 | 11 | The following are the explicit goals of *libman*: 12 | 13 | 1. Define a series of file formats which tell a build system how a library is 14 | "used" 15 | 2. Define the semantics of how a build system should interact and perform 16 | name-based package and dependency lookup in a deterministic fashion with no 17 | dependence on "ambient" environment state 18 | 3. Implement minimal tools and libraries for consuming and generating the files 19 | defined by *libman* 20 | 21 | Non-Goals 22 | ********* 23 | 24 | Perhaps just as important as the goals are the *non-goals.* In particular, 25 | *libman does not* seek to do any of the following: 26 | 27 | 1. Define the semantics of ABI and version compatibility between libraries 28 | 2. Facilitate dependency resolution beyond trivial name-based path lookup 29 | 3. Define a distribution or packaging method for pre-compiled binary packages 30 | 4. Define or aide package retrieval and extraction 31 | 5. Define or aide source-package building 32 | 33 | The Format 34 | ********** 35 | 36 | *libman* defines a file format and schema for the files that will be consumed 37 | by a build system or generated by a PDM. See the :ref:`lm.files` page for 38 | details. 39 | 40 | -------------------------------------------------------------------------------- /docs/package-file.rst: -------------------------------------------------------------------------------- 1 | .. _lm.package: 2 | 3 | Package Files 4 | ############# 5 | 6 | .. note:: 7 | Read :ref:`lm.format` first. 8 | 9 | A *package* is a collection that contains one or more *libraries* that are 10 | distributed and built as a unit. The package file describes the contents of a 11 | single package. 12 | 13 | The file extension of a package file is ``.lmp``. 14 | 15 | 16 | Fields 17 | ****** 18 | 19 | 20 | ``Type`` 21 | ======== 22 | 23 | The ``Type`` field in a package file *must* be ``Package``, and must appear 24 | *exactly once*:: 25 | 26 | Type: Package 27 | 28 | 29 | ``Name`` 30 | ======== 31 | 32 | The ``Name`` field in a package file should be the name of the package, and 33 | *should* match the name of the package present in the :ref:`index ` 34 | that points to the file defining the package. If ``Name`` is not present or not 35 | equal to the name provided in the index, consumers are not required to generate 36 | a warning. It's purpose is for the querying of individual package files and for 37 | human consumption. 38 | 39 | :: 40 | 41 | Name: Boost 42 | 43 | 44 | ``Namespace`` 45 | ============= 46 | 47 | The ``Namespace`` field in a package file is require to appear *exactly once*. 48 | It is not required to correspond to any C++ ``namespace``, and is purely for the 49 | namespaces of the import information for consuming tools. For example, CMake 50 | will prepend the ``Namespace`` and two colons ``::`` to the name of imported 51 | targets generated from the *libman* manifests. 52 | 53 | :: 54 | 55 | Namespace: Qt5 56 | 57 | .. note:: 58 | The namespace is not required to be unique between packages. Multiple 59 | packages may declare themselves to share a ``Namespace``, such as for 60 | modularized Boost packages. 61 | 62 | 63 | ``Requires`` 64 | ============ 65 | 66 | The ``Requires`` field may appear multiple times, each time specifying the name 67 | of a package which is required in order to use the requiring package. 68 | 69 | When a consumer encounters a ``Requires`` field, they should use the 70 | :ref:`index file ` to find the package specified by the given name. 71 | If no such package is listed in the index, the consumer should generate an 72 | error. 73 | 74 | :: 75 | 76 | Requires: Boost.Filesystem 77 | Requires: Boost.Coroutine2 78 | Requires: fmtlib 79 | 80 | 81 | ``Library`` 82 | =========== 83 | 84 | The ``Library`` field specifies the path to a :ref:`library file `. 85 | Each appearance of the ``Library`` field specifies another library which should 86 | be considered as part of the package. 87 | 88 | :: 89 | 90 | Library: filesystem.lml 91 | Library: system.lml 92 | Library: coroutine2.lml 93 | 94 | If a relative path, the file path should be resolved relative to the directory 95 | of the package file. 96 | 97 | .. note:: 98 | The filename of a ``Library`` field is not significant. 99 | 100 | 101 | Example 102 | ******* 103 | 104 | .. code-block:: yaml 105 | 106 | # A merged Qt5 107 | Type: Package 108 | Name: Qt5 109 | Namespace: Qt5 110 | 111 | # Some things we might require 112 | Requires: OpenSSL 113 | Requires: Xcb 114 | 115 | # Qt libraries 116 | Library: Core.lml 117 | Library: Widgets.lml 118 | Library: Gui.lml 119 | Library: Network.lml 120 | Library: Quick.lml 121 | # ... (Qt has many libraries) 122 | 123 | 124 | Rationale and Intended Usage 125 | **************************** 126 | 127 | While many projects out there will only expose a single library, it is important 128 | to support the use case of frameworks both large and small. We can't assume that 129 | a single package exposes a single consumable/linkable, nor can we assume that 130 | a package exports something linkable *at all.* For example, a package may be 131 | distributed only to contain enhancements to an underlying build tool, 132 | to enable code generation (Done using ``X-`` "extension fields"), or to act as 133 | "meta" packages meant to purely depend on a collection of other packages. 134 | 135 | The ``Namespace`` field is meant to allow individual libraries to use 136 | unqualified names without colliding with a global names. 137 | 138 | Upon importing the usage requirements of the libraries within a package, the 139 | identities of the imported libraries should be qualified the the ``Namespace`` 140 | of the package in which the library is defined. 141 | 142 | The package files may or may not be generated on-the-fly by a tool, either at 143 | install time or build time. The package files may also be hand-written and 144 | bundled with the binary distribution of the package. This can be useful for 145 | closed-source packages that wish to distribute a package which is compatible 146 | with *libman*-aware build systems and dependency managers. 147 | -------------------------------------------------------------------------------- /extras/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | option(BUILD_PYTHON_LIB "Build the python library" ON) 2 | if(BUILD_PYTHON_LIB) 3 | add_subdirectory(libman-py) 4 | endif() 5 | -------------------------------------------------------------------------------- /extras/libman-py/.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.pyc 3 | __pycache__/ 4 | .tox/ 5 | .mypy_cache/ 6 | .pytest_cache/ 7 | dist/ 8 | build/ -------------------------------------------------------------------------------- /extras/libman-py/.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code 6 | extension-pkg-whitelist= 7 | 8 | # Add files or directories to the blacklist. They should be base names, not 9 | # paths. 10 | ignore=CVS 11 | 12 | # Add files or directories matching the regex patterns to the blacklist. The 13 | # regex matches against base names, not paths. 14 | ignore-patterns= 15 | 16 | # Python code to execute, usually for sys.path manipulation such as 17 | # pygtk.require(). 18 | #init-hook= 19 | 20 | # Use multiple processes to speed up Pylint. 21 | jobs=1 22 | 23 | # List of plugins (as comma separated values of python modules names) to load, 24 | # usually to register additional checkers. 25 | load-plugins= 26 | 27 | # Pickle collected data for later comparisons. 28 | persistent=yes 29 | 30 | # Specify a configuration file. 31 | #rcfile= 32 | 33 | # Allow loading of arbitrary C extensions. Extensions are imported into the 34 | # active Python interpreter and may run arbitrary code. 35 | unsafe-load-any-extension=no 36 | 37 | 38 | [MESSAGES CONTROL] 39 | 40 | # Only show warnings with the listed confidence levels. Leave empty to show 41 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 42 | confidence= 43 | 44 | # Disable the message, report, category or checker with the given id(s). You 45 | # can either give multiple identifiers separated by comma (,) or put this 46 | # option multiple times (only on the command line, not in the configuration 47 | # file where it should appear only once).You can also use "--disable=all" to 48 | # disable everything first and then reenable specific checks. For example, if 49 | # you want to run only the similarities checker, you can use "--disable=all 50 | # --enable=similarities". If you want to run only the classes checker, but have 51 | # no Warning level messages displayed, use"--disable=all --enable=classes 52 | # --disable=W" 53 | disable=I,too-few-public-methods,line-too-long,missing-final-newline,len-as-condition,too-few-public-methods 54 | 55 | # Enable the message, report, category or checker with the given id(s). You can 56 | # either give multiple identifier separated by comma (,) or put this option 57 | # multiple time (only on the command line, not in the configuration file where 58 | # it should appear only once). See also the "--disable" option for examples. 59 | enable= 60 | 61 | 62 | [REPORTS] 63 | 64 | # Python expression which should return a note less than 10 (10 is the highest 65 | # note). You have access to the variables errors warning, statement which 66 | # respectively contain the number of errors / warnings messages and the total 67 | # number of statements analyzed. This is used by the global evaluation report 68 | # (RP0004). 69 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 70 | 71 | # Template used to display messages. This is a python new-style format string 72 | # used to format the message information. See doc for all details 73 | #msg-template= 74 | 75 | # Set the output format. Available formats are text, parseable, colorized, json 76 | # and msvs (visual studio).You can also give a reporter class, eg 77 | # mypackage.mymodule.MyReporterClass. 78 | output-format=text 79 | 80 | # Tells whether to display a full report or only the messages 81 | reports=no 82 | 83 | # Activate the evaluation score. 84 | score=yes 85 | 86 | 87 | [REFACTORING] 88 | 89 | # Maximum number of nested blocks for function / method body 90 | max-nested-blocks=5 91 | 92 | 93 | [MISCELLANEOUS] 94 | 95 | # List of note tags to take in consideration, separated by a comma. 96 | notes=FIXME,XXX,TODO 97 | 98 | 99 | [SPELLING] 100 | 101 | # Spelling dictionary name. Available dictionaries: none. To make it working 102 | # install python-enchant package. 103 | spelling-dict= 104 | 105 | # List of comma separated words that should not be checked. 106 | spelling-ignore-words= 107 | 108 | # A path to a file that contains private dictionary; one word per line. 109 | spelling-private-dict-file= 110 | 111 | # Tells whether to store unknown words to indicated private dictionary in 112 | # --spelling-private-dict-file option instead of raising a message. 113 | spelling-store-unknown-words=no 114 | 115 | 116 | [TYPECHECK] 117 | 118 | # List of decorators that produce context managers, such as 119 | # contextlib.contextmanager. Add to this list to register other decorators that 120 | # produce valid context managers. 121 | contextmanager-decorators=contextlib.contextmanager 122 | 123 | # List of members which are set dynamically and missed by pylint inference 124 | # system, and so shouldn't trigger E1101 when accessed. Python regular 125 | # expressions are accepted. 126 | generated-members= 127 | 128 | # Tells whether missing members accessed in mixin class should be ignored. A 129 | # mixin class is detected if its name ends with "mixin" (case insensitive). 130 | ignore-mixin-members=yes 131 | 132 | # This flag controls whether pylint should warn about no-member and similar 133 | # checks whenever an opaque object is returned when inferring. The inference 134 | # can return multiple potential results while evaluating a Python object, but 135 | # some branches might not be evaluated, which results in partial inference. In 136 | # that case, it might be useful to still emit no-member and other checks for 137 | # the rest of the inferred objects. 138 | ignore-on-opaque-inference=yes 139 | 140 | # List of class names for which member attributes should not be checked (useful 141 | # for classes with dynamically set attributes). This supports the use of 142 | # qualified names. 143 | ignored-classes=optparse.Values,thread._local,_thread._local 144 | 145 | # List of module names for which member attributes should not be checked 146 | # (useful for modules/projects where namespaces are manipulated during runtime 147 | # and thus existing member attributes cannot be deduced by static analysis. It 148 | # supports qualified module names, as well as Unix pattern matching. 149 | ignored-modules= 150 | 151 | # Show a hint with possible names when a member name was not found. The aspect 152 | # of finding the hint is based on edit distance. 153 | missing-member-hint=yes 154 | 155 | # The minimum edit distance a name should have in order to be considered a 156 | # similar match for a missing member name. 157 | missing-member-hint-distance=1 158 | 159 | # The total number of similar names that should be taken in consideration when 160 | # showing a hint for a missing member. 161 | missing-member-max-choices=1 162 | 163 | 164 | [VARIABLES] 165 | 166 | # List of additional names supposed to be defined in builtins. Remember that 167 | # you should avoid to define new builtins when possible. 168 | additional-builtins= 169 | 170 | # Tells whether unused global variables should be treated as a violation. 171 | allow-global-unused-variables=yes 172 | 173 | # List of strings which can identify a callback function by name. A callback 174 | # name must start or end with one of those strings. 175 | callbacks=cb_,_cb 176 | 177 | # A regular expression matching the name of dummy variables (i.e. expectedly 178 | # not used). 179 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 180 | 181 | # Argument names that match this expression will be ignored. Default to name 182 | # with leading underscore 183 | ignored-argument-names=_.*|^ignored_|^unused_ 184 | 185 | # Tells whether we should check for unused import in __init__ files. 186 | init-import=no 187 | 188 | # List of qualified module names which can have objects that can redefine 189 | # builtins. 190 | redefining-builtins-modules=six.moves,future.builtins 191 | 192 | 193 | [FORMAT] 194 | 195 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 196 | expected-line-ending-format= 197 | 198 | # Regexp for a line that is allowed to be longer than the limit. 199 | ignore-long-lines=^\s*(# )??$ 200 | 201 | # Number of spaces of indent required inside a hanging or continued line. 202 | indent-after-paren=4 203 | 204 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 205 | # tab). 206 | indent-string=' ' 207 | 208 | # Maximum number of characters on a single line. 209 | max-line-length=100 210 | 211 | # Maximum number of lines in a module 212 | max-module-lines=1000 213 | 214 | # List of optional constructs for which whitespace checking is disabled. `dict- 215 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 216 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 217 | # `empty-line` allows space-only lines. 218 | no-space-check=trailing-comma,dict-separator 219 | 220 | # Allow the body of a class to be on the same line as the declaration if body 221 | # contains single statement. 222 | single-line-class-stmt=no 223 | 224 | # Allow the body of an if to be on the same line as the test if there is no 225 | # else. 226 | single-line-if-stmt=no 227 | 228 | 229 | [LOGGING] 230 | 231 | # Logging modules to check that the string format arguments are in logging 232 | # function parameter format 233 | logging-modules=logging 234 | 235 | 236 | [SIMILARITIES] 237 | 238 | # Ignore comments when computing similarities. 239 | ignore-comments=yes 240 | 241 | # Ignore docstrings when computing similarities. 242 | ignore-docstrings=yes 243 | 244 | # Ignore imports when computing similarities. 245 | ignore-imports=no 246 | 247 | # Minimum lines number of a similarity. 248 | min-similarity-lines=4 249 | 250 | 251 | [BASIC] 252 | 253 | # Naming hint for argument names 254 | argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 255 | 256 | # Regular expression matching correct argument names 257 | argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 258 | 259 | # Naming hint for attribute names 260 | attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 261 | 262 | # Regular expression matching correct attribute names 263 | attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 264 | 265 | # Bad variable names which should always be refused, separated by a comma 266 | bad-names=foo,bar,baz,toto,tutu,tata 267 | 268 | # Naming hint for class attribute names 269 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 270 | 271 | # Regular expression matching correct class attribute names 272 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 273 | 274 | # Naming hint for class names 275 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 276 | 277 | # Regular expression matching correct class names 278 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 279 | 280 | # Naming hint for constant names 281 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 282 | 283 | # Regular expression matching correct constant names 284 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 285 | 286 | # Minimum line length for functions/classes that require docstrings, shorter 287 | # ones are exempt. 288 | docstring-min-length=-1 289 | 290 | # Naming hint for function names 291 | function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 292 | 293 | # Regular expression matching correct function names 294 | function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 295 | 296 | # Good variable names which should always be accepted, separated by a comma 297 | good-names=i,j,k,ex,Run,_,fd 298 | 299 | # Include a hint for the correct naming format with invalid-name 300 | include-naming-hint=no 301 | 302 | # Naming hint for inline iteration names 303 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 304 | 305 | # Regular expression matching correct inline iteration names 306 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 307 | 308 | # Naming hint for method names 309 | method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 310 | 311 | # Regular expression matching correct method names 312 | method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 313 | 314 | # Naming hint for module names 315 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 316 | 317 | # Regular expression matching correct module names 318 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 319 | 320 | # Colon-delimited sets of names that determine each other's naming style when 321 | # the name regexes allow several styles. 322 | name-group= 323 | 324 | # Regular expression which should only match function or class names that do 325 | # not require a docstring. 326 | no-docstring-rgx=^_ 327 | 328 | # List of decorators that produce properties, such as abc.abstractproperty. Add 329 | # to this list to register other decorators that produce valid properties. 330 | property-classes=abc.abstractproperty 331 | 332 | # Naming hint for variable names 333 | variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 334 | 335 | # Regular expression matching correct variable names 336 | variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 337 | 338 | 339 | [IMPORTS] 340 | 341 | # Allow wildcard imports from modules that define __all__. 342 | allow-wildcard-with-all=no 343 | 344 | # Analyse import fallback blocks. This can be used to support both Python 2 and 345 | # 3 compatible code, which means that the block might have code that exists 346 | # only in one or another interpreter, leading to false positives when analysed. 347 | analyse-fallback-blocks=no 348 | 349 | # Deprecated modules which should not be used, separated by a comma 350 | deprecated-modules=optparse,tkinter.tix 351 | 352 | # Create a graph of external dependencies in the given file (report RP0402 must 353 | # not be disabled) 354 | ext-import-graph= 355 | 356 | # Create a graph of every (i.e. internal and external) dependencies in the 357 | # given file (report RP0402 must not be disabled) 358 | import-graph= 359 | 360 | # Create a graph of internal dependencies in the given file (report RP0402 must 361 | # not be disabled) 362 | int-import-graph= 363 | 364 | # Force import order to recognize a module as part of the standard 365 | # compatibility libraries. 366 | known-standard-library= 367 | 368 | # Force import order to recognize a module as part of a third party library. 369 | known-third-party=enchant 370 | 371 | 372 | [CLASSES] 373 | 374 | # List of method names used to declare (i.e. assign) instance attributes. 375 | defining-attr-methods=__init__,__new__,setUp 376 | 377 | # List of member names, which should be excluded from the protected access 378 | # warning. 379 | exclude-protected=_asdict,_fields,_replace,_source,_make 380 | 381 | # List of valid names for the first argument in a class method. 382 | valid-classmethod-first-arg=cls 383 | 384 | # List of valid names for the first argument in a metaclass class method. 385 | valid-metaclass-classmethod-first-arg=mcs 386 | 387 | 388 | [DESIGN] 389 | 390 | # Maximum number of arguments for function / method 391 | max-args=5 392 | 393 | # Maximum number of attributes for a class (see R0902). 394 | max-attributes=7 395 | 396 | # Maximum number of boolean expressions in a if statement 397 | max-bool-expr=5 398 | 399 | # Maximum number of branch for function / method body 400 | max-branches=12 401 | 402 | # Maximum number of locals for function / method body 403 | max-locals=15 404 | 405 | # Maximum number of parents for a class (see R0901). 406 | max-parents=7 407 | 408 | # Maximum number of public methods for a class (see R0904). 409 | max-public-methods=20 410 | 411 | # Maximum number of return / yield for function / method body 412 | max-returns=6 413 | 414 | # Maximum number of statements in function / method body 415 | max-statements=50 416 | 417 | # Minimum number of public methods for a class (see R0903). 418 | min-public-methods=2 419 | 420 | 421 | [EXCEPTIONS] 422 | 423 | # Exceptions that will emit a warning when being caught. Defaults to 424 | # "Exception" 425 | overgeneral-exceptions=Exception 426 | -------------------------------------------------------------------------------- /extras/libman-py/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | option(BUILD_LIBMAN_PY "Build the Python components" ON) 2 | if(NOT BUILD_LIBMAN_PY) 3 | return() 4 | endif() 5 | 6 | option(DEFINE_LIBMAN_PY_TESTS "Defined the libman-py tests" ON) 7 | 8 | find_program(TOX_EXECUTABLE tox DOC "Path to tox, for running Python tests") 9 | 10 | find_package(Python3 COMPONENTS Interpreter) 11 | find_program(WHEEL_EXECUTABLE wheel) 12 | 13 | if(NOT TARGET Python3::Interpreter) 14 | message(SEND_ERROR "No Python, so libman-py cannot be built") 15 | return() 16 | endif() 17 | 18 | get_target_property(py Python3::Interpreter LOCATION) 19 | execute_process( 20 | COMMAND "${py}" -m wheel --help 21 | OUTPUT_VARIABLE out 22 | ERROR_VARIABLE out 23 | RESULT_VARIABLE rc 24 | ) 25 | if(rc) 26 | message(SEND_ERROR "The chosen Python (${py}) does not have the 'wheel' module installed, but it is required") 27 | return() 28 | endif() 29 | 30 | file(GLOB_RECURSE sources 31 | CONFIGURE_DEPENDS 32 | FOLLOW_SYMLINKS false 33 | "libman/*.py" 34 | setup.py 35 | Pipfile 36 | Pipfile.lock 37 | mypy.ini 38 | .pylintrc 39 | tox.ini 40 | ) 41 | 42 | get_filename_component(wheel_stamp "${CMAKE_CURRENT_BINARY_DIR}/libman-wheel.stamp" ABSOLUTE) 43 | 44 | add_custom_command( 45 | OUTPUT "${wheel_stamp}" 46 | DEPENDS ${sources} 47 | COMMAND Python3::Interpreter setup.py -q bdist_wheel 48 | COMMAND ${CMAKE_COMMAND} -E touch ${wheel_stamp} 49 | WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}" 50 | COMMENT "Generating libman-py wheel" 51 | ) 52 | 53 | add_custom_target(libman-wheel ALL DEPENDS "${wheel_stamp}") 54 | 55 | if(NOT DEFINE_LIBMAN_PY_TESTS) 56 | return() 57 | endif() 58 | 59 | if(NOT TOX_EXECUTABLE) 60 | message(SEND_ERROR "No `tox` installed, so we can't run the tests.") 61 | else() 62 | add_test( 63 | NAME py.tox.libman-py 64 | COMMAND "${TOX_EXECUTABLE}" 65 | WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" 66 | ) 67 | endif() 68 | -------------------------------------------------------------------------------- /extras/libman-py/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | 8 | [dev-packages] 9 | libman = {editable = true, path = "."} 10 | tox = "*" 11 | pylint = "*" 12 | yapf = "*" 13 | invoke = "*" 14 | mypy = "*" 15 | pytest = "*" 16 | 17 | [requires] 18 | python_version = "3.6" 19 | -------------------------------------------------------------------------------- /extras/libman-py/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "f60afa74ab13e3047c3a5310563329e1973d19ec179b590e42dda2c7ab4b6d15" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": {}, 19 | "develop": { 20 | "astroid": { 21 | "hashes": [ 22 | "sha256:292fa429e69d60e4161e7612cb7cc8fa3609e2e309f80c224d93a76d5e7b58be", 23 | "sha256:c7013d119ec95eb626f7a2011f0b63d0c9a095df9ad06d8507b37084eada1a8d" 24 | ], 25 | "version": "==2.0.4" 26 | }, 27 | "atomicwrites": { 28 | "hashes": [ 29 | "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", 30 | "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee" 31 | ], 32 | "version": "==1.2.1" 33 | }, 34 | "attrs": { 35 | "hashes": [ 36 | "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", 37 | "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" 38 | ], 39 | "version": "==18.2.0" 40 | }, 41 | "filelock": { 42 | "hashes": [ 43 | "sha256:86fe6af56ae08ebc9c66d54ba3398c35b98916d0862d782b276a65816ff39392", 44 | "sha256:97694f181bdf58f213cca0a7cb556dc7bf90e2f8eb9aa3151260adac56701afb" 45 | ], 46 | "version": "==3.0.9" 47 | }, 48 | "invoke": { 49 | "hashes": [ 50 | "sha256:4f4de934b15c2276caa4fbc5a3b8a61c0eb0b234f2be1780d2b793321995c2d6", 51 | "sha256:dc492f8f17a0746e92081aec3f86ae0b4750bf41607ea2ad87e5a7b5705121b7", 52 | "sha256:eb6f9262d4d25b40330fb21d1e99bf0f85011ccc3526980f8a3eaedd4b43892e" 53 | ], 54 | "index": "pypi", 55 | "version": "==1.2.0" 56 | }, 57 | "isort": { 58 | "hashes": [ 59 | "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", 60 | "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", 61 | "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" 62 | ], 63 | "version": "==4.3.4" 64 | }, 65 | "lazy-object-proxy": { 66 | "hashes": [ 67 | "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", 68 | "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", 69 | "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", 70 | "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", 71 | "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", 72 | "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", 73 | "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", 74 | "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", 75 | "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", 76 | "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", 77 | "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", 78 | "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", 79 | "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", 80 | "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", 81 | "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", 82 | "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", 83 | "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", 84 | "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", 85 | "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", 86 | "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", 87 | "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", 88 | "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", 89 | "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", 90 | "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", 91 | "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", 92 | "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", 93 | "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", 94 | "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", 95 | "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" 96 | ], 97 | "version": "==1.3.1" 98 | }, 99 | "libman": { 100 | "editable": true, 101 | "path": "." 102 | }, 103 | "mccabe": { 104 | "hashes": [ 105 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 106 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 107 | ], 108 | "version": "==0.6.1" 109 | }, 110 | "more-itertools": { 111 | "hashes": [ 112 | "sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092", 113 | "sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e", 114 | "sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d" 115 | ], 116 | "version": "==4.3.0" 117 | }, 118 | "mypy": { 119 | "hashes": [ 120 | "sha256:8e071ec32cc226e948a34bbb3d196eb0fd96f3ac69b6843a5aff9bd4efa14455", 121 | "sha256:fb90c804b84cfd8133d3ddfbd630252694d11ccc1eb0166a1b2efb5da37ecab2" 122 | ], 123 | "index": "pypi", 124 | "version": "==0.641" 125 | }, 126 | "mypy-extensions": { 127 | "hashes": [ 128 | "sha256:37e0e956f41369209a3d5f34580150bcacfabaa57b33a15c0b25f4b5725e0812", 129 | "sha256:b16cabe759f55e3409a7d231ebd2841378fb0c27a5d1994719e340e4f429ac3e" 130 | ], 131 | "version": "==0.4.1" 132 | }, 133 | "pluggy": { 134 | "hashes": [ 135 | "sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095", 136 | "sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f" 137 | ], 138 | "version": "==0.8.0" 139 | }, 140 | "py": { 141 | "hashes": [ 142 | "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", 143 | "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6" 144 | ], 145 | "version": "==1.7.0" 146 | }, 147 | "pylint": { 148 | "hashes": [ 149 | "sha256:1d6d3622c94b4887115fe5204982eee66fdd8a951cf98635ee5caee6ec98c3ec", 150 | "sha256:31142f764d2a7cd41df5196f9933b12b7ee55e73ef12204b648ad7e556c119fb" 151 | ], 152 | "index": "pypi", 153 | "version": "==2.1.1" 154 | }, 155 | "pytest": { 156 | "hashes": [ 157 | "sha256:10e59f84267370ab20cec9305bafe7505ba4d6b93ecbf66a1cce86193ed511d5", 158 | "sha256:8c827e7d4816dfe13e9329c8226aef8e6e75d65b939bc74fda894143b6d1df59" 159 | ], 160 | "index": "pypi", 161 | "version": "==3.9.1" 162 | }, 163 | "six": { 164 | "hashes": [ 165 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", 166 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" 167 | ], 168 | "version": "==1.11.0" 169 | }, 170 | "toml": { 171 | "hashes": [ 172 | "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", 173 | "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" 174 | ], 175 | "version": "==0.10.0" 176 | }, 177 | "tox": { 178 | "hashes": [ 179 | "sha256:217fb84aecf9792a98f93f07cfcaf014205a76c64e52bd7c2b4135458e6ad2a1", 180 | "sha256:4baeb3d8ebdcd9f43afce38aa67d06f1165a87d221d5bb21e8b39a0d4880c134" 181 | ], 182 | "index": "pypi", 183 | "version": "==3.5.2" 184 | }, 185 | "typed-ast": { 186 | "hashes": [ 187 | "sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58", 188 | "sha256:10703d3cec8dcd9eef5a630a04056bbc898abc19bac5691612acba7d1325b66d", 189 | "sha256:1f6c4bd0bdc0f14246fd41262df7dfc018d65bb05f6e16390b7ea26ca454a291", 190 | "sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a", 191 | "sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9", 192 | "sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892", 193 | "sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9", 194 | "sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded", 195 | "sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa", 196 | "sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe", 197 | "sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd", 198 | "sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85", 199 | "sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6", 200 | "sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46", 201 | "sha256:898f818399cafcdb93cbbe15fc83a33d05f18e29fb498ddc09b0214cdfc7cd51", 202 | "sha256:94b091dc0f19291adcb279a108f5d38de2430411068b219f41b343c03b28fb1f", 203 | "sha256:a26863198902cda15ab4503991e8cf1ca874219e0118cbf07c126bce7c4db129", 204 | "sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c", 205 | "sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea", 206 | "sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863", 207 | "sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559", 208 | "sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87", 209 | "sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6" 210 | ], 211 | "markers": "python_version < '3.7' and implementation_name == 'cpython'", 212 | "version": "==1.1.0" 213 | }, 214 | "virtualenv": { 215 | "hashes": [ 216 | "sha256:2ce32cd126117ce2c539f0134eb89de91a8413a29baac49cbab3eb50e2026669", 217 | "sha256:ca07b4c0b54e14a91af9f34d0919790b016923d157afda5efdde55c96718f752" 218 | ], 219 | "version": "==16.0.0" 220 | }, 221 | "wrapt": { 222 | "hashes": [ 223 | "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" 224 | ], 225 | "version": "==1.10.11" 226 | }, 227 | "yapf": { 228 | "hashes": [ 229 | "sha256:b96815bd0bbd2ab290f2ae9e610756940b17a0523ef2f6b2d31da749fc395137", 230 | "sha256:cebb6faf35c9027c08996c07831b8971f3d67c0eb615269f66dfd7e6815fdc2a" 231 | ], 232 | "index": "pypi", 233 | "version": "==0.24.0" 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /extras/libman-py/libman/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/libman/088ea18796dc664260a5b2dcf4d92a83b08188c6/extras/libman-py/libman/__init__.py -------------------------------------------------------------------------------- /extras/libman-py/libman/__main__.py: -------------------------------------------------------------------------------- 1 | """Entrypoint for using ``python -m libman``""" 2 | from .main import _start 3 | 4 | if __name__ == '__main__': 5 | _start() 6 | -------------------------------------------------------------------------------- /extras/libman-py/libman/data.py: -------------------------------------------------------------------------------- 1 | """ 2 | Data types for dealing with libman files 3 | """ 4 | 5 | from typing import Iterable, Dict, Optional, List, Tuple, Set, Mapping, cast, Iterator, Type 6 | from pathlib import Path 7 | 8 | import dataclasses as dc 9 | 10 | 11 | class InvalidDataError(RuntimeError): 12 | """Base class for libman data validation errors""" 13 | 14 | 15 | class InvalidIndexError(InvalidDataError): 16 | """Exception for data validation failures with a libman index""" 17 | 18 | 19 | class InvalidPackageError(InvalidDataError): 20 | """Exception for data validation failures in a libman package file""" 21 | 22 | 23 | class InvalidLibraryError(InvalidDataError): 24 | """Exception for data validation failures in a libman library file""" 25 | 26 | 27 | class Field: 28 | """ 29 | Represents a field in a libman file 30 | """ 31 | 32 | def __init__(self, key: str, value: str) -> None: 33 | self._key = key 34 | self._value = value 35 | 36 | @property 37 | def key(self) -> str: 38 | """The key for the field""" 39 | return self._key 40 | 41 | @property 42 | def value(self) -> str: 43 | """The value string of of the field""" 44 | return self._value 45 | 46 | def __hash__(self): 47 | return hash((self.key, self.value)) 48 | 49 | def __repr__(self): 50 | return f'' 51 | 52 | 53 | class FieldSequence: 54 | """ 55 | Represents an ordered sequence of fields 56 | """ 57 | 58 | def __init__(self, fields: Iterable[Field]): 59 | self._fields = list(fields) 60 | self._by_key: Dict[str, List[Field]] = {} 61 | for field in self._fields: 62 | seq = self._by_key[field.key] = self._by_key.get(field.key, []) 63 | seq.append(field) 64 | 65 | @property 66 | def fields(self) -> Iterable[Field]: 67 | """The fields in the sequence""" 68 | return (f for f in self._fields) 69 | 70 | def __iter__(self) -> Iterator[Field]: 71 | for field in self._fields: 72 | yield field 73 | 74 | def for_key(self, key: str) -> Iterable[Field]: 75 | """Iterable of all fields in the sequence with the given key""" 76 | found = self._by_key.get(key) 77 | if not found: 78 | return () 79 | return found 80 | 81 | def get_at_most_one( 82 | self, 83 | key: str, 84 | exc: Type[RuntimeError] = RuntimeError, 85 | ) -> Optional[Field]: 86 | """ 87 | Get the value of the given field, if present. 88 | 89 | If the field is absent, returns ``None``. 90 | 91 | If more than one instance of the field occurs, raises ``exc`` 92 | """ 93 | found = self._by_key.get(key) 94 | if not found: 95 | return None 96 | if len(found) != 1: 97 | raise exc(f'Field "{key}" provided more than once') 98 | return found[0] 99 | 100 | def get_exactly_one( 101 | self, 102 | key: str, 103 | exc: Type[RuntimeError] = RuntimeError, 104 | ) -> Field: 105 | """ 106 | Get _exactly_ the value of the given field. 107 | 108 | If the field is not present or appears multiple times, raises ``exc`` 109 | """ 110 | found = self.get_at_most_one(key, exc) 111 | if not found: 112 | raise exc(f'Missing field "{key}"') 113 | return found 114 | 115 | 116 | class IndexEntry: 117 | """ 118 | An entry in the libman index 119 | """ 120 | 121 | def __init__(self, name: str, path: Path) -> None: 122 | self._name = name 123 | self._path = path 124 | 125 | @property 126 | def name(self): 127 | """The name of the package for this index entry""" 128 | return self._name 129 | 130 | @property 131 | def path(self): 132 | """The path to the file for this package""" 133 | return self._path 134 | 135 | def __hash__(self): 136 | return hash((self.name, self.path)) 137 | 138 | def __repr__(self): 139 | return f' IndexEntry: 159 | return self.entries[key] 160 | 161 | def get(self, key: str) -> Optional[IndexEntry]: 162 | "Get the index entry for the given package name" 163 | return self.entries.get(key) 164 | 165 | def __contains__(self, key: str) -> bool: 166 | return key in self.entries 167 | 168 | @classmethod 169 | def from_fields(cls, fields: FieldSequence, filepath: Path) -> 'Index': 170 | """ 171 | Convert a sequence of fields into an Index 172 | """ 173 | # Singular values 174 | type_ = fields.get_exactly_one('Type', InvalidIndexError).value 175 | if type_ != 'Index': 176 | raise InvalidIndexError(f'Invlaid "Type" for index file: {type_}') 177 | # Parse the index entries 178 | entries: List[IndexEntry] = [] 179 | already: Set[str] = set() 180 | for field in fields: 181 | if field.key == 'Package': 182 | if not ';' in field.value: 183 | raise InvalidIndexError( 184 | f'Invalid "Package" field in index file: {repr(field)}' 185 | ) 186 | pkg_name, pkg_path_str = field.value.split(';', 1) 187 | pkg_name, pkg_path = pkg_name.strip(), \ 188 | filepath.parent / Path(pkg_path_str.strip()) 189 | if pkg_name in already: 190 | raise InvalidIndexError( 191 | 'Cannot provided package name "{}" multiple times'. 192 | format(pkg_name)) 193 | already.add(pkg_name) 194 | entries.append(IndexEntry(pkg_name, pkg_path)) 195 | 196 | return cls({e.name: e for e in entries}, fields) 197 | 198 | 199 | @dc.dataclass(frozen=True) 200 | class Package: 201 | """ 202 | A libman package 203 | """ 204 | name: str 205 | namespace: str 206 | requires: Iterable[str] 207 | libraries: Iterable[Path] 208 | fields: FieldSequence 209 | 210 | @classmethod 211 | def from_fields(cls, fields: FieldSequence, filepath: Path) -> 'Package': 212 | """ 213 | Convert the given fields into a ``Package`` definition 214 | """ 215 | # Check that we are a package type 216 | type_ = fields.get_exactly_one('Type', InvalidPackageError).value 217 | if type_ != 'Package': 218 | raise InvalidPackageError( 219 | f'Package file declares incorrect Type "{type_}"') 220 | namespace = fields.get_exactly_one('Namespace', 221 | InvalidPackageError).value 222 | name = fields.get_exactly_one('Name', InvalidPackageError).value 223 | libraries: List[Path] = [ 224 | filepath.parent / f.value for f in fields.for_key('Library') 225 | ] 226 | requires: List[str] = [f.value for f in fields.for_key('Requires')] 227 | return cls( 228 | name, 229 | namespace, 230 | list(requires), 231 | list(libraries), 232 | fields, 233 | ) 234 | 235 | 236 | @dc.dataclass(frozen=True) 237 | class Library: 238 | """ 239 | A libman library 240 | """ 241 | name: str 242 | path: Optional[Path] 243 | includes: Iterable[Path] 244 | defines: Iterable[str] 245 | uses: Iterable[Tuple[str, str]] 246 | links: Iterable[Tuple[str, str]] 247 | fields: FieldSequence 248 | 249 | @classmethod 250 | def from_fields(cls, fields: FieldSequence, filepath: Path) -> 'Library': 251 | """ 252 | Create a ``Library`` instance from a list of fields, assuming it was 253 | defined by the given file at ``filepath``. 254 | 255 | :param fields: The fields to create the library definition from 256 | :param filepath: The path where the original fields were read (used 257 | to resolve relative paths) 258 | """ 259 | type_ = fields.get_exactly_one('Type', InvalidLibraryError).value 260 | if type_ != 'Library': 261 | raise InvalidLibraryError( 262 | f'Library file declares incorrect Type "{type_}"') 263 | name = fields.get_exactly_one('Name', InvalidLibraryError).value 264 | path_ = fields.get_at_most_one('Path', InvalidLibraryError) 265 | path: Optional[Path] 266 | if path_: 267 | path = filepath.parent / path_.value 268 | else: 269 | path = None 270 | includes = [ 271 | filepath.parent / f.value for f in fields.for_key('Include') 272 | ] 273 | defines = [f.value for f in fields.for_key('Define')] 274 | 275 | def split_req(req: str) -> Tuple[str, str]: 276 | seq = req.split('/') 277 | if not len(seq) == 2: 278 | raise InvalidLibraryError( 279 | 'Invalid usage name "{}" (espect "/")'. 280 | format(req)) 281 | return cast(Tuple[str, str], tuple(seq)) 282 | 283 | uses = [split_req(f.value) for f in fields.for_key('Uses')] 284 | links = [split_req(f.value) for f in fields.for_key('Links')] 285 | return cls(name, path, includes, defines, uses, links, fields) 286 | -------------------------------------------------------------------------------- /extras/libman-py/libman/data_test.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=C 2 | 3 | from typing import Iterable, Tuple, Type, Optional, List 4 | from pathlib import Path 5 | from dataclasses import dataclass, field 6 | 7 | import pytest 8 | 9 | from . import data as mod 10 | 11 | INDEX_PATH = Path('dummy/libman.lmi') 12 | PKG_PATH = Path('dummy/libman.lmp') 13 | 14 | 15 | def make_fieldseq(*pairs: Tuple[str, str]) -> mod.FieldSequence: 16 | """Helper for creating a field sequence""" 17 | return mod.FieldSequence(mod.Field(k, v) for k, v in pairs) 18 | 19 | 20 | @dataclass(frozen=True) 21 | class IndexTestCase: 22 | fields: Iterable[Tuple[str, str]] 23 | path: Path = Path('dummy/libman.lmi') 24 | expect_exception: Optional[Type[RuntimeError]] = None 25 | expect_packages: Iterable[Tuple[str, Path]] = field( 26 | default_factory=lambda: []) 27 | 28 | def create_index(self) -> mod.Index: 29 | return mod.Index.from_fields(make_fieldseq(*self.fields), self.path) 30 | 31 | 32 | def test_index(): 33 | cases: Iterable[IndexTestCase] = [ 34 | IndexTestCase( 35 | # Missing 'Type' 36 | fields=[], 37 | expect_exception=mod.InvalidIndexError, 38 | ), 39 | IndexTestCase(fields=[('Type', 'Index')]), 40 | IndexTestCase( 41 | fields=[ 42 | ('Type', 'Index'), 43 | ('Package', 'Meow'), # <-- Missing path 44 | ], 45 | expect_exception=mod.InvalidIndexError, 46 | ), 47 | IndexTestCase( 48 | fields=[ 49 | ('Type', 'Index'), 50 | ('Package', 'Meow; something/somewhere'), 51 | # Duplicate package name: 52 | ('Package', 'Meow; /absolute/path/somewhere'), 53 | ], 54 | expect_exception=mod.InvalidIndexError, 55 | ), 56 | IndexTestCase( 57 | fields=[ 58 | ('Type', 'Index'), 59 | ('Package', 'Meow; something/somewhere'), 60 | ('Package', 'Meow2; /absolute/path/somewhere'), 61 | ], 62 | expect_packages=[ 63 | ('Meow', Path('dummy/something/somewhere')), 64 | ('Meow2', Path('/absolute/path/somewhere')), 65 | ], 66 | ), 67 | ] 68 | for case in cases: 69 | if case.expect_exception: 70 | with pytest.raises(case.expect_exception): 71 | case.create_index() 72 | else: 73 | idx = case.create_index() 74 | assert len(idx) == len(case.expect_packages), \ 75 | 'Wrong number of packages parsed' 76 | for actual, expected in zip(idx, case.expect_packages): 77 | exp_name, exp_path = expected 78 | assert actual.name == exp_name, 'Package name parsed wrong' 79 | assert actual.path == exp_path, 'Package path parsed wrong' 80 | 81 | 82 | @dataclass(frozen=True) 83 | class PackageTestCase: 84 | fields: Iterable[Tuple[str, str]] 85 | path: Path = Path('/dummy/package.lmp') 86 | expect_exception: Optional[Type[RuntimeError]] = None 87 | expect_libraries: Iterable[Path] = field(default_factory=lambda: []) 88 | 89 | def create_package(self) -> mod.Package: 90 | return mod.Package.from_fields(make_fieldseq(*self.fields), self.path) 91 | 92 | 93 | def test_packages(): 94 | cases: List[PackageTestCase] = [ 95 | PackageTestCase( 96 | # Empty. Missing "Type" 97 | fields=[], 98 | expect_exception=mod.InvalidPackageError, 99 | ), 100 | PackageTestCase( 101 | fields=[ 102 | ('Type', 'Library'), # <-- Wrong type 103 | ('Name', 'Meow'), 104 | ('Namespace', 'Boost'), 105 | ], 106 | expect_exception=mod.InvalidPackageError, 107 | ), 108 | PackageTestCase( 109 | fields=[ 110 | ('Type', 'Package'), 111 | ('Name', 'Meow'), 112 | # Missing 'Namespace' 113 | ## ('Namespace', 'Cat'), 114 | ], 115 | expect_exception=mod.InvalidPackageError, 116 | ), 117 | PackageTestCase( 118 | fields=[ 119 | ('Type', 'Package'), 120 | # Missing 'Name' 121 | ## ('Name', 'Meow'), 122 | ('Namespace', 'Cat'), 123 | ], 124 | expect_exception=mod.InvalidPackageError, 125 | ), 126 | ] 127 | for case in cases: 128 | if case.expect_exception: 129 | with pytest.raises(case.expect_exception): 130 | case.create_package() 131 | else: 132 | pkg = case.create_package() 133 | assert case.expect_libraries == pkg.libraries 134 | 135 | 136 | @dataclass(frozen=True) 137 | class LibraryTestCase: 138 | fields: Iterable[Tuple[str, str]] 139 | path: Path = Path('/dummy/library.lmi') 140 | expect_name: Optional[str] = None 141 | expect_path: Optional[Path] = None 142 | expect_exception: Optional[Type[RuntimeError]] = None 143 | expect_includes: Iterable[Path] = field(default_factory=lambda: []) 144 | expect_defines: Iterable[str] = field(default_factory=lambda: []) 145 | expect_uses: Iterable[Tuple[str, str]] = field(default_factory=lambda: []) 146 | expect_links: Iterable[Tuple[str, str]] = field(default_factory=lambda: []) 147 | 148 | def create_library(self) -> mod.Library: 149 | return mod.Library.from_fields(make_fieldseq(*self.fields), self.path) 150 | 151 | 152 | def test_libraries(): 153 | cases: Iterable[LibraryTestCase] = [ 154 | LibraryTestCase( 155 | fields=[], 156 | expect_exception=mod.InvalidLibraryError, 157 | ), 158 | LibraryTestCase( 159 | # Invalid type 160 | fields=[('Type', 'Package')], 161 | expect_exception=mod.InvalidLibraryError, 162 | ), 163 | LibraryTestCase( 164 | # Missing 'Name' 165 | fields=[('Type', 'Library')], 166 | expect_exception=mod.InvalidLibraryError, 167 | ), 168 | LibraryTestCase( 169 | fields=[ 170 | ('Type', 'Library'), 171 | ('Name', 'Foo2'), 172 | ], 173 | expect_name='Foo2', 174 | ), 175 | LibraryTestCase( 176 | fields=[ 177 | ('Type', 'Library'), 178 | ('Name', 'Foo'), 179 | ('Include', 'some/path'), 180 | ('Include', 'some/other/path'), 181 | ('Uses', 'foo/bar'), 182 | ], 183 | expect_name='Foo', 184 | expect_includes=[ 185 | Path('/dummy/some/path'), 186 | Path('/dummy/some/other/path'), 187 | ], 188 | expect_uses=[ 189 | ('foo', 'bar'), 190 | ], 191 | ), 192 | ] 193 | for case in cases: 194 | if case.expect_exception: 195 | with pytest.raises(case.expect_exception): 196 | case.create_library() 197 | else: 198 | lib = case.create_library() 199 | assert lib.path == case.expect_path 200 | assert lib.includes == case.expect_includes 201 | assert lib.defines == case.expect_defines 202 | assert lib.uses == case.expect_uses 203 | assert lib.links == case.expect_links 204 | -------------------------------------------------------------------------------- /extras/libman-py/libman/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | Defines the entrypoint for the ``libman`` command-line tool 3 | """ 4 | import argparse 5 | import sys 6 | from pathlib import Path 7 | from typing import List 8 | 9 | from . import parse 10 | 11 | 12 | def _query_index(args: argparse.Namespace) -> int: 13 | index_path = Path(args.index).resolve() 14 | index = parse.parse_index_file(index_path) 15 | if args.query == 'has-package': 16 | return 0 if args.package in index else 1 17 | if args.query == 'package-path': 18 | item = index.get(args.package) 19 | if not item: 20 | print('No such package:', args.package, file=sys.stderr) 21 | return 2 22 | print(item.path) 23 | return 0 24 | 25 | raise RuntimeError('No query type?') 26 | 27 | 28 | def _add_query_index_parser(parser: argparse.ArgumentParser): 29 | parser.set_defaults(main_fn=_query_index) 30 | parser.add_argument( 31 | '--query', 32 | '-Q', 33 | required=True, 34 | help='The query type', 35 | choices=['has-package', 'package-path'], 36 | ) 37 | parser.add_argument( 38 | '--index', 39 | '-I', 40 | required=True, 41 | help='Path to the index file', 42 | ) 43 | parser.add_argument( 44 | '--package', 45 | '-p', 46 | required=True, 47 | help='Path to the index file', 48 | ) 49 | 50 | 51 | def _query_package(args: argparse.Namespace) -> int: 52 | pkg_path = Path(args.package).resolve() 53 | pkg = parse.parse_package_file(pkg_path) 54 | if args.query == 'namespace': 55 | print(pkg.namespace) 56 | return 0 57 | if args.query == 'name': 58 | print(pkg.name) 59 | return 0 60 | if args.query == 'requires': 61 | for req in pkg.requires: 62 | print(req) 63 | return 0 64 | if args.query == 'libraries': 65 | for lib in pkg.libraries: 66 | print(lib) 67 | return 0 68 | if args.query == 'key': 69 | if args.key is None: 70 | raise RuntimeError('No --key argument was specified') 71 | for field in pkg.fields: 72 | if field.key == args.key: 73 | print(field.value) 74 | return 0 75 | assert False, 'Unhandled query type: ' + args.query 76 | return 109 77 | 78 | 79 | def _add_query_package_parser(parser: argparse.ArgumentParser): 80 | parser.set_defaults(main_fn=_query_package) 81 | parser.add_argument( 82 | '--query', 83 | '-Q', 84 | required=True, 85 | help='The query type', 86 | choices=['namespace', 'name', 'requires', 'libraries', 'key'], 87 | ) 88 | parser.add_argument( 89 | '--package', 90 | '-p', 91 | required=True, 92 | help='Path to a package file', 93 | ) 94 | parser.add_argument( 95 | '--key', 96 | help='Query a different package key (Used with --query=key)', 97 | ) 98 | 99 | 100 | def _query_library(args: argparse.Namespace) -> int: 101 | # pylint: disable=too-many-return-statements,too-many-branches 102 | lib_path = Path(args.library).resolve() 103 | lib = parse.parse_library_file(lib_path) 104 | if args.query == 'name': 105 | print(lib.name) 106 | return 0 107 | if args.query == 'path': 108 | print(lib.path) 109 | return 0 110 | if args.query == 'includes': 111 | for inc in lib.includes: 112 | print(inc) 113 | return 0 114 | if args.query == 'defines': 115 | for define in lib.defines: 116 | print(define) 117 | return 0 118 | if args.query == 'uses': 119 | for use in lib.uses: 120 | print(f'{use[0]}/{use[1]}') 121 | return 0 122 | if args.query == 'links': 123 | for use in lib.links: 124 | print(f'{use[0]}/{use[1]}') 125 | return 0 126 | if args.query == 'key': 127 | if args.key is None: 128 | raise RuntimeError('No --key argument was specified') 129 | for field in lib.fields: 130 | if field.key == args.key: 131 | print(field.value) 132 | return 0 133 | 134 | assert False, f'Unknown query type: {args.query}' 135 | return 14 136 | 137 | 138 | def _add_library_package_parser(parser: argparse.ArgumentParser): 139 | parser.set_defaults(main_fn=_query_library) 140 | parser.add_argument( 141 | '--query', 142 | '-Q', 143 | required=True, 144 | help='The query type', 145 | choices=[ 146 | 'name', 147 | 'path', 148 | 'includes', 149 | 'defines', 150 | 'uses', 151 | 'links', 152 | 'key', 153 | ], 154 | ) 155 | parser.add_argument( 156 | '--library', 157 | '-l', 158 | required=True, 159 | help='Path to a library file', 160 | ) 161 | parser.add_argument( 162 | '--key', 163 | help='Query a package key (Used with --query=key)', 164 | ) 165 | 166 | 167 | def _add_query_parser(parser: argparse.ArgumentParser): 168 | sub = parser.add_subparsers(title='Query Command') 169 | _add_query_index_parser( 170 | sub.add_parser( 171 | 'index', 172 | aliases=['i', 'idx'], 173 | help='Query an Index file', 174 | )) 175 | _add_query_package_parser( 176 | sub.add_parser( 177 | 'package', 178 | aliases=['p', 'pkg'], 179 | help='Query a Package file', 180 | )) 181 | _add_library_package_parser( 182 | sub.add_parser( 183 | 'library', 184 | aliases=['l', 'lib'], 185 | help='Query a Library file', 186 | )) 187 | 188 | 189 | def create_argument_parser() -> argparse.ArgumentParser: 190 | """ 191 | Create a command-line argument parser for the libman tool 192 | """ 193 | parser = argparse.ArgumentParser() 194 | sub = parser.add_subparsers(title='Command') 195 | _add_query_parser( 196 | sub.add_parser( 197 | 'query', 198 | aliases=['q'], 199 | help='Query libman files', 200 | )) 201 | return parser 202 | 203 | 204 | def main(argv: List[str]) -> int: 205 | """ 206 | The main function for the libman command-line tool 207 | """ 208 | parser = create_argument_parser() 209 | args = parser.parse_args(argv) 210 | if not hasattr(args, 'main_fn'): 211 | parser.print_help() 212 | return 1 213 | return args.main_fn(args) 214 | 215 | 216 | def _start(): 217 | sys.exit(main(sys.argv[1:])) 218 | -------------------------------------------------------------------------------- /extras/libman-py/libman/parse.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for parsing libman files 3 | """ 4 | 5 | from os import PathLike 6 | from pathlib import Path 7 | from contextlib import contextmanager 8 | from typing import IO, Iterable, Optional, Union, cast, ContextManager, Generator 9 | 10 | from . import data 11 | 12 | 13 | def parse_line(line: str) -> Optional[data.Field]: 14 | """ 15 | Parse a single line from a libman document 16 | """ 17 | if isinstance(line, bytes): 18 | line = line.decode() 19 | line = line.strip() 20 | if line.startswith('#'): 21 | # Comment, so ignore 22 | return None 23 | if line == '': 24 | # Empty line 25 | return None 26 | col_pos = line.find(': ') 27 | if col_pos == -1: 28 | col_pos = line.find(':') 29 | if col_pos == -1 or col_pos != len(line) - 1: 30 | raise ValueError(f'Invalid libman line: "{line}"') 31 | key, value = line[:col_pos], line[col_pos + 1:] 32 | key, value = key.strip(), value.strip() 33 | return data.Field(key, value) 34 | 35 | 36 | def iter_fields_from_lines(lines: Iterable[str]) -> Iterable[data.Field]: 37 | """ 38 | Lazily iterate over the fields present in the given string lines of a 39 | libman-format file. 40 | """ 41 | for line in lines: 42 | field = parse_line(line) 43 | if field: 44 | yield field 45 | 46 | 47 | def iter_string_fields(doc: str) -> Iterable[data.Field]: 48 | """ 49 | Lazily parse fields from the given document string 50 | """ 51 | return iter_fields_from_lines(doc.splitlines()) 52 | 53 | 54 | def parse_string(doc: str) -> data.FieldSequence: 55 | """ 56 | Parse all of the fields in the given string and return a FieldSequence 57 | """ 58 | return data.FieldSequence(iter_string_fields(doc)) 59 | 60 | 61 | #: Type for things which can be "opened" or treated like files 62 | LibmanFile = Union[str, PathLike, IO] 63 | 64 | 65 | @contextmanager 66 | def _fake_file_ctx_man(item: IO) -> Generator[IO, None, None]: 67 | yield item 68 | 69 | 70 | def open_as_file(what: LibmanFile) -> ContextManager[IO]: 71 | """ 72 | Given a "file-like," return an IO object for reading. 73 | 74 | If given a path or string, opens the file. 75 | 76 | If given a file object, returns the file. 77 | 78 | This should be used in a context-manager fashion. The file will only be 79 | closed if we opened it within this function. 80 | """ 81 | if hasattr(what, '__fspath__') or isinstance(what, str): 82 | return Path(cast(PathLike, what)).open('rb') 83 | # If not a string or path, we expect a file-like object 84 | assert hasattr(what, 'readlines'), \ 85 | f'Expected a file-like object or file path, got: {repr(what)}' 86 | # We wrap the file so that people that use this as a context manager do 87 | # not close the file that we didn't open ourselves 88 | return _fake_file_ctx_man(cast(IO, what)) 89 | 90 | 91 | def iter_file_fields(doc: LibmanFile) -> Iterable[data.Field]: 92 | """ 93 | Lazily parse the fields from the given file or filepath 94 | """ 95 | with open_as_file(doc) as fd: 96 | return iter_fields_from_lines(fd.readlines()) 97 | 98 | 99 | def parse_file(doc: LibmanFile) -> data.FieldSequence: 100 | """ 101 | Parse the given file into a FieldSequence 102 | """ 103 | return data.FieldSequence(iter_file_fields(doc)) 104 | 105 | 106 | def parse_index_string(doc: str, filepath: Path) -> data.Index: 107 | """ 108 | Parse an index from the given string 109 | """ 110 | return data.Index.from_fields(parse_string(doc), filepath) 111 | 112 | 113 | def parse_index_file(fpath: Path) -> data.Index: 114 | """ 115 | Parse an index from a file 116 | """ 117 | return data.Index.from_fields(parse_file(fpath), fpath) 118 | 119 | 120 | def parse_package_string(doc: str, filepath: Path) -> data.Package: 121 | """ 122 | Parse a package from the given string 123 | """ 124 | return data.Package.from_fields(parse_string(doc), filepath) 125 | 126 | 127 | def parse_package_file(fpath: Path) -> data.Package: 128 | """ 129 | Parse a package from the given file 130 | """ 131 | return data.Package.from_fields(parse_file(fpath), fpath) 132 | 133 | 134 | def parse_library_string(doc: str, filepath: Path) -> data.Library: 135 | """ 136 | Parse a library from the given string 137 | """ 138 | return data.Library.from_fields(parse_string(doc), filepath) 139 | 140 | 141 | def parse_library_file(fpath: Path) -> data.Library: 142 | """ 143 | Parse a library from the given file 144 | """ 145 | return data.Library.from_fields(parse_file(fpath), fpath) 146 | -------------------------------------------------------------------------------- /extras/libman-py/libman/parse_test.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=C 2 | 3 | from pathlib import Path 4 | 5 | import pytest 6 | 7 | from . import parse as mod 8 | 9 | 10 | def test_empty(): 11 | fields = list(mod.iter_string_fields('')) 12 | assert fields == [] 13 | 14 | 15 | def test_simple_line(): 16 | field = mod.parse_line('foo: bar') 17 | assert field 18 | assert field.key == 'foo' 19 | assert field.value == 'bar' 20 | 21 | 22 | def test_extra_whitespace(): 23 | field = mod.parse_line(' foo: bar ') 24 | assert field 25 | assert field.key == 'foo' 26 | assert field.value == 'bar' 27 | 28 | 29 | def test_empty_value(): 30 | field = mod.parse_line('foo: ') 31 | assert field 32 | assert field.key == 'foo' 33 | assert field.value == '' 34 | 35 | 36 | def test_empty_value_at_eof(): 37 | field = mod.parse_line('foo:') 38 | assert field 39 | assert field.key == 'foo' 40 | assert field.value == '' 41 | 42 | 43 | def test_empty_value_at_nl(): 44 | (field, ) = list(mod.iter_string_fields('foo:\n\n')) 45 | assert field.key == 'foo' 46 | assert field.value == '' 47 | 48 | 49 | def test_ignore_comments(): 50 | (field, ) = list(mod.iter_string_fields('# Comment line\nfoo: bar')) 51 | assert field.key == 'foo' 52 | assert field.value == 'bar' 53 | 54 | 55 | def test_bad_line(): 56 | with pytest.raises(ValueError): 57 | mod.parse_line('food') 58 | 59 | 60 | def test_key_with_colon(): 61 | field = mod.parse_line('foo:bar: baz') 62 | assert field 63 | assert field.key == 'foo:bar' 64 | assert field.value == 'baz' 65 | 66 | 67 | def test_no_trailing_comment(): 68 | field = mod.parse_line('foo: # bar') 69 | assert field 70 | assert field.key == 'foo' 71 | assert field.value == '# bar' 72 | 73 | 74 | def test_key_with_whitespace(): 75 | field = mod.parse_line('Foo Bar: Baz') 76 | assert field 77 | assert field.key == 'Foo Bar' 78 | assert field.value == 'Baz' 79 | 80 | 81 | def test_parse_index(): 82 | content = r''' 83 | Type: Index 84 | Package: foo ; /bar/baz 85 | Package: Meow ;cat/relative/path 86 | ''' 87 | idx = mod.parse_index_string(content, Path('/dummy/something.lmi')) 88 | assert len(idx) == 2 89 | # Check that we have the foo package correctly 90 | entry = idx['foo'] 91 | assert entry.name == 'foo' 92 | assert entry.path == Path('/bar/baz') 93 | # Check our that our "Meow" package relative path resolved 94 | entry = idx['Meow'] 95 | assert entry.name == 'Meow' 96 | assert entry.path == Path('/dummy/cat/relative/path') 97 | 98 | 99 | def test_parse_index_duplicate(): 100 | content = r''' 101 | Type: Index 102 | Package: foo; bar 103 | Package: foo; baz 104 | ''' 105 | # We have a duplicate package, so we will fail 106 | with pytest.raises(RuntimeError): 107 | mod.parse_index_string(content, Path('/dummy/foo.lmi')) 108 | -------------------------------------------------------------------------------- /extras/libman-py/libman/util.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/libman/088ea18796dc664260a5b2dcf4d92a83b08188c6/extras/libman-py/libman/util.py -------------------------------------------------------------------------------- /extras/libman-py/mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | strict_optional=True 3 | incremental=True 4 | ignore_missing_imports=True -------------------------------------------------------------------------------- /extras/libman-py/setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Setup for libman, the library manifest format library. 3 | """ 4 | 5 | from setuptools import setup, find_packages 6 | 7 | setup( 8 | name='libman', 9 | version='0.1.0', 10 | packages=find_packages(), 11 | requires=['dataclasses'], 12 | ) 13 | -------------------------------------------------------------------------------- /extras/libman-py/tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py36 3 | 4 | [testenv] 5 | commands = 6 | pipenv install --dev 7 | pylint libman \ 8 | -fcolorized \ 9 | --reports=no \ 10 | -dI 11 | mypy libman 12 | pytest libman 13 | deps = 14 | pipenv 15 | ; We must install project requirements explicitly (Tox bug or feature?) 16 | dataclasses 17 | -------------------------------------------------------------------------------- /lm_conan/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for libman conan support""" -------------------------------------------------------------------------------- /lm_conan/cmake.py: -------------------------------------------------------------------------------- 1 | import conans 2 | from pathlib import Path 3 | import shutil 4 | from typing import Set, Optional 5 | 6 | from .export import package_exports, ExportCopier 7 | 8 | 9 | def cmake_build(cf: conans.ConanFile, **kwargs): 10 | """ 11 | Build the libman-aware project in the provided ConanFile with CMake. Build 12 | the ``libman-export`` target. 13 | 14 | :param conans.ConanFile cf: The ConanFile defining the project. 15 | :param kwargs: Keyword arguments forwarded to the ``conans.CMake`` constructor. 16 | """ 17 | cmake = conans.CMake(cf, **kwargs) 18 | cmake.build_folder = 'cmake-build' 19 | cmake.configure() 20 | cmake.build(target='libman-export') 21 | 22 | 23 | class CMakeConanFile(ExportCopier, conans.ConanFile): 24 | def build(self): 25 | cmake_build(self) 26 | -------------------------------------------------------------------------------- /lm_conan/data.py: -------------------------------------------------------------------------------- 1 | from os import PathLike 2 | from pathlib import Path 3 | from typing import List 4 | 5 | class Library: 6 | def __init__( 7 | self, 8 | name: str, 9 | paths: List[Path], 10 | includes: List[Path], 11 | defines: List[str], 12 | uses: List[str], 13 | special_uses: List[str], 14 | infos: List[str], 15 | warnings: List[str], 16 | ): 17 | self.name = name 18 | self.paths = paths 19 | self.include_paths = includes 20 | self.defines = defines 21 | self.uses = uses 22 | self.special_uses = special_uses 23 | self.infos = infos 24 | self.warnings = warnings 25 | 26 | def get_lines(self) -> List[str]: 27 | lines = [ 28 | '# libman library file generated by Conan. DO NOT EDIT!', 29 | 'Type: Library', 30 | f'Name: {self.name}', 31 | ] 32 | for inc in self.include_paths: 33 | lines.append(f'Include-Path: {inc}') 34 | for def_ in self.defines: 35 | lines.append(f'Preprocessor-Define: {def_}') 36 | for path in self.paths: 37 | lines.append(f'Path: {path}') 38 | for special in self.special_uses: 39 | lines.append(f'Special-Uses: {special}') 40 | for uses in self.uses: 41 | lines.append(f'Uses: {uses}') 42 | return lines 43 | 44 | def write(self, fpath: PathLike) -> None: 45 | lines = self.get_lines() 46 | content = '\n'.join(lines) 47 | with Path(fpath).open('wb') as fd: 48 | fd.write(content) 49 | -------------------------------------------------------------------------------- /lm_conan/export.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for handling .libman-export directories 3 | """ 4 | 5 | import conans 6 | from pathlib import Path 7 | import shutil 8 | 9 | from typing import Optional, Set 10 | 11 | 12 | class AlreadyPackaged(RuntimeError): 13 | pass 14 | 15 | 16 | def package_exports(cf: conans.ConanFile, exported: Optional[Set[Path]] = None) -> Set[Path]: 17 | """ 18 | Copy any libman export roots to the package directory for the Conan project. 19 | 20 | :param conans.ConanFile cf: The ConanFile that is being exported. 21 | :param Optional[Set[Path]] exported: A set of export roots that have 22 | already been exported 23 | 24 | :returns: The set of paths that have been exported, including those provided 25 | in the ``exported`` argument. 26 | 27 | .. note:: 28 | If more than one export root directory has the same filename stem as 29 | another export root directory, the packaging will fail with an 30 | exception. 31 | """ 32 | exported = set(exported or set()) 33 | 34 | lm_exports = list(Path(cf.build_folder).glob('**/*.libman-export')) 35 | 36 | exported_names = set(exp.name for exp in exported) 37 | for export in lm_exports: 38 | if export in exported: 39 | # This directory was already exported once. Don't export it again 40 | continue 41 | # Check that another export with the same name hasn't already been copied 42 | cf.output.info(f'Packaging libman export "{export.stem}" ({export})') 43 | if export.name in exported_names: 44 | raise AlreadyPackaged(f'More than one export directory with name "{export.stem}"!') 45 | # Calc the destination for the export and do the copy 46 | dest = Path(cf.package_folder) / export.name 47 | shutil.copytree(export, dest) 48 | # Record what has been exported 49 | exported.add(export) 50 | exported_names.add(export.name) 51 | 52 | # Return the new set of exported directories 53 | return exported 54 | 55 | 56 | class ExportCopier: 57 | def __init__(self, *args, **kwargs): 58 | super().__init__(*args, **kwargs) 59 | self.__already_exported = set() 60 | 61 | def package_exports(self): 62 | assert getattr(self, 'package_folder', None), 'No package_folder is defined' 63 | assert isinstance(self, conans.ConanFile), 'ExportCopier derived classes must also derive from ConanFile' 64 | self.__already_exported = package_exports(self, self.__already_exported) 65 | assert len(self.__already_exported) > 0, 'No directories have been exported' 66 | 67 | def package(self): 68 | self.package_exports() 69 | 70 | def package_info(self): 71 | self.user_info.libman_simple = True 72 | -------------------------------------------------------------------------------- /lm_conan/generator.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from typing import Dict, List, Optional, Union, Iterable 4 | from pathlib import Path 5 | from itertools import chain 6 | 7 | import conans 8 | import conans.model 9 | from conans.model.user_info import DepsUserInfo, UserInfo 10 | from conans.model.build_info import DepsCppInfo, CppInfo 11 | 12 | from .data import Library 13 | from .vs import generate_msbuild_props 14 | 15 | 16 | class LibManLibrary(Library): 17 | @classmethod 18 | def find_linkable(cls, lib: str, lib_paths: List[str]) -> Optional[Path]: 19 | for lib_path in lib_paths: 20 | lib_path = Path(lib_path) 21 | candidates = chain( 22 | lib_path.glob(f'lib{lib}.a'), 23 | lib_path.glob(f'lib{lib}.lib'), 24 | lib_path.glob(f'lib{lib}.so'), 25 | lib_path.glob(f'{lib}.dll'), 26 | ) 27 | try: 28 | return next(iter(candidates)) 29 | except StopIteration: 30 | pass 31 | # No linkable found 32 | return None 33 | 34 | @classmethod 35 | def generate_default( 36 | cls, name: str, 37 | cpp_info: conans.model.build_info.CppInfo) -> 'LibManLibrary': 38 | include_paths = [Path(p) for p in cpp_info.include_paths] 39 | defines = list(cpp_info.defines) 40 | paths: List[Path] = [] 41 | specials: List[str] = [] 42 | infos: List[str] = [] 43 | warnings: List[str] = [] 44 | uses = [] # We don't fill this out for default-generated libs 45 | for lib in cpp_info.libs: 46 | # Generate a path item for each library 47 | special_by_lib = { 48 | 'pthread': 'Threading', 49 | 'dl': 'DynamicLinking', 50 | 'm': 'Math', 51 | } 52 | special = special_by_lib.get(lib) 53 | if special: 54 | infos.append( 55 | f'Link to `{lib}` being interpreted as special requirement "{special}"' 56 | ) 57 | specials.append(special) 58 | else: 59 | found = cls.find_linkable(lib, cpp_info.lib_paths) 60 | if found: 61 | warnings.append( 62 | f'Library has no libman metadata and was generated automatically: {found}' 63 | ) 64 | paths.append(found) 65 | else: 66 | warnings.append(f'Unresolved library {name}') 67 | return LibManLibrary( 68 | name, 69 | paths, 70 | include_paths, 71 | defines, 72 | uses, 73 | specials, 74 | infos, 75 | warnings, 76 | ) 77 | 78 | 79 | class AutoPackage: 80 | def __init__(self, name: str, ns: str, requires: List[str], 81 | libs: List[LibManLibrary]) -> None: 82 | self.name = name 83 | self.namespace = ns 84 | self.requires = requires 85 | self.libs = libs 86 | self.has_libman_data = False 87 | 88 | @staticmethod 89 | def create( 90 | name: str, 91 | cpp_info: CppInfo, 92 | user_info: UserInfo, 93 | ) -> 'AutoPackage': 94 | reqs = list(cpp_info.public_deps) 95 | ns = name 96 | libs = [LibManLibrary.generate_default(name, cpp_info)] 97 | return AutoPackage(name, ns, reqs, libs) 98 | 99 | def _generate_library_file(self, all_pkgs: Dict[str, 'AutoPackage'], 100 | lib: LibManLibrary) -> Dict[str, str]: 101 | lines = lib.get_lines() 102 | # The package did not expose libman data, so we must generate some 103 | # important information ourselves manually 104 | for req in self.requires: 105 | other_pkg = all_pkgs[req] 106 | if not getattr(other_pkg, 'has_libman_data', False): 107 | lines.append(f'Uses: {other_pkg.name}/{other_pkg.name}') 108 | else: 109 | for other_lib in other_pkg.libs: 110 | lines.append( 111 | f'Uses: {other_pkg.namespace}/{other_lib.name}') 112 | 113 | lml_path = f'{self.name}-libs/{lib.name}.lml' 114 | return {f'lm/{lml_path}': '\n'.join(lines)}, lml_path 115 | 116 | def generate_files(self, 117 | pkgs: Dict[str, 'AutoPackage']) -> Dict[str, str]: 118 | lines = [ 119 | '# Libman package file generated by Conan. DO NOT EDIT', 120 | 'Type: Package', 121 | f'Name: {self.name}', 122 | f'Namespace: {self.namespace}', 123 | ] 124 | for req in self.requires: 125 | lines.append(f'Requires: {req}') 126 | 127 | lmp_path = f'lm/{self.name}.lmp' 128 | ret = {} 129 | for lib in self.libs: 130 | more, lml_path = self._generate_library_file(pkgs, lib) 131 | ret.update(more) 132 | lines.append(f'Library: {lml_path}') 133 | ret[lmp_path] = '\n'.join(lines) 134 | return ret, lmp_path 135 | 136 | 137 | class ExportRootPackage: 138 | def __init__(self, name: str, root: Path): 139 | self.name = name 140 | self.root = root 141 | 142 | 143 | class MetadataPackage: 144 | def __init__(self, name: str, root: Path, data: dict): 145 | self.name = name 146 | self.root = root 147 | packages = data.get('packages', []) 148 | if not isinstance(packages, list): 149 | raise TypeError(f'The libman metadata assoicated with {name} is invalid ("packages" should be a list)') 150 | 151 | self.packages = [] 152 | for pkg_data in packages: 153 | if not isinstance(pkg_data, dict): 154 | raise TypeError(f'The libman metadata with {name} is invalid (elements of "packages" should be dicts)') 155 | self.packages.append(pkg_data) 156 | 157 | 158 | AnyPackage = Union[AutoPackage, ExportRootPackage, MetadataPackage] 159 | 160 | 161 | class Generator(conans.model.Generator): 162 | """ 163 | The libman Conan generator 164 | 165 | This class generates a libman tree an index from the requirements installed 166 | by a Conan installation. The file it writes, ``conan.lmi`` is intended to be 167 | be a build-system-agnostic representation of how to important consume the 168 | libraries that Conan has installed. 169 | """ 170 | 171 | def __init__(self, *args, **kwargs) -> None: 172 | super().__init__(*args, **kwargs) 173 | #: The output logger 174 | self.output = self.conanfile.output 175 | 176 | @property 177 | def filename(self): 178 | """Unused. See the content() method.""" 179 | pass 180 | 181 | def _load_packages( 182 | self, 183 | cpp_infos: DepsCppInfo, 184 | user_infos: DepsUserInfo, 185 | ) -> Dict[str, AnyPackage]: 186 | ret = {} 187 | for name, cpp_info in cpp_infos.dependencies: 188 | user_info = user_infos[name] 189 | if user_info.vars.get('libman_simple'): 190 | ret[name] = ExportRootPackage(name, cpp_info.rootpath) 191 | elif user_info.vars.get('libman'): 192 | ret[name] = MetadataPackage(name, cpp_info.rootpath, json.loads(user_info.libman)) 193 | else: 194 | ret[name] = AutoPackage.create(name, cpp_info, user_info) 195 | return ret 196 | 197 | def _generate_from_deps_info( 198 | self, 199 | cpp_infos: DepsCppInfo, 200 | user_infos: DepsUserInfo, 201 | ) -> Dict[str, str]: 202 | """ 203 | Generate the libman files from some Conan dependency information. 204 | 205 | :param conans.model.build_info.DepsCppInfo cpp_infos: 206 | The C++ information from the installed dependencies 207 | :param conans.model.user_info.DepsUserInfo user_infos: 208 | The user information attached to the dependencies 209 | 210 | :returns: A dict mapping from files to create and the content thereof. 211 | """ 212 | # The lines of the index 213 | index_lines = [ 214 | 'Type: Index', 215 | ] 216 | all_pkgs = self._load_packages(cpp_infos, user_infos) 217 | 218 | # Accumulator for files that will will be filling out 219 | ret = {} 220 | for name, pkg in all_pkgs.items(): 221 | if isinstance(pkg, ExportRootPackage): 222 | for lmp in Path(pkg.root).glob('*.libman-export/*.lmp'): 223 | index_lines.append(f'Package: {lmp.stem}; {lmp}') 224 | elif isinstance(pkg, MetadataPackage): 225 | for pkg_data in pkg.packages: 226 | pkg_name = pkg_data['name'] 227 | pkg_path = Path(pkg_data['path']) 228 | if not pkg_path.is_absolute(): 229 | pkg_path = Path(pkg.root) / pkg_path 230 | index_lines.append(f'Package: {pkg_name}; {pkg_path}') 231 | else: 232 | more_files, lmp_path = pkg.generate_files(all_pkgs) 233 | ret.update(more_files) 234 | index_lines.append(f'Package: {name}; {lmp_path}') 235 | for lib in pkg.libs: 236 | for info in lib.infos: 237 | self.output.info(f'{pkg.name}/{lib.name}: {info}') 238 | for warning in lib.warnings: 239 | self.output.warn(f'{pkg.name}/{lib.name}: {warning}') 240 | 241 | ret['INDEX.lmi'] = '\n'.join(index_lines) 242 | 243 | # Generate build-system specific helper files if they have been requested 244 | lm_for = getattr(self.conanfile, 'libman_for', []) 245 | if isinstance(lm_for, str): 246 | lm_for = [lm_for] 247 | # Generate for each libman generator 248 | for lm_gen in lm_for: 249 | if lm_gen == 'cmake': 250 | lm_pkg_dir = self.deps_build_info['libman'].rootpath 251 | libman_cmake = (Path(lm_pkg_dir) / 'libman.cmake').read_text() 252 | ret['libman.cmake'] = libman_cmake 253 | elif lm_gen in ('vs', 'visualstudio'): 254 | msb_prop_content = generate_msbuild_props(ret.copy(), self.conanfile) 255 | fname: str = getattr(self.conanfile, 'libman_vs_filename', 'Libman.targets') 256 | vs_platform = { 257 | 'x86': 'Win32', 258 | 'x86_64': 'x64', 259 | None: 'NoArch', 260 | }[str(self.conanfile.settings.arch)] 261 | fname = fname.format(settings=self.conanfile.settings, vs_platform=vs_platform) 262 | ret[fname] = msb_prop_content 263 | elif lm_for is None: 264 | pass 265 | else: 266 | raise RuntimeError(f'Unknown `libman_for` value: {lm_for}') 267 | 268 | return ret 269 | 270 | @property 271 | def content(self): 272 | return self._generate_from_deps_info(self.deps_build_info, 273 | self.deps_user_info) 274 | -------------------------------------------------------------------------------- /lm_conan/vs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Generate MSBuild properties files from the libman index data 3 | """ 4 | 5 | import os 6 | import re 7 | from pathlib import Path 8 | from typing import Dict, Optional, List 9 | from xml.dom import minidom 10 | from xml.etree import ElementTree as ET 11 | 12 | from conans import ConanFile 13 | from conans.client.output import ConanOutput 14 | 15 | 16 | def _add_library(prop_group: ET.Element, autodefs: ET.Element, ns: str, lmp_path: Path, 17 | lml_path: Path, data: Dict[str, str], out: ConanOutput) -> None: 18 | if not lml_path.is_absolute(): 19 | lml_path = lmp_path.parent / lml_path 20 | 21 | if lml_path.is_absolute(): 22 | lml_content = lml_path.read_text() 23 | else: 24 | lml_content = data[str(lml_path.as_posix())] 25 | 26 | NAME_LEAD = 'Name: ' 27 | PATH_LEAD = 'Path: ' 28 | INCLUDE_LEAD = 'Include-Path: ' 29 | name: str = '' 30 | lib_path: str = '' 31 | includes: List[str] = [] 32 | for line_ in lml_content.splitlines(): 33 | line: str = line_.strip() 34 | if line.startswith(NAME_LEAD): 35 | name = line[len(NAME_LEAD):].strip() 36 | elif line.startswith(PATH_LEAD): 37 | lib_path = line[len(PATH_LEAD):] 38 | elif line.startswith(INCLUDE_LEAD): 39 | includes.append(line[len(INCLUDE_LEAD):] + '\\') 40 | elif line.startswith('Type: '): 41 | assert 'Library' in line 42 | elif line == '': 43 | pass 44 | else: 45 | out.warn(f'Unhandled lml line: {line}') 46 | 47 | if not name: 48 | raise RuntimeError(f'Missing name in lml: {lml_path}') 49 | 50 | def _resolve(p: str) -> str: 51 | if os.path.isabs(p): 52 | return p 53 | else: 54 | return str(lml_path.parent / p) 55 | 56 | # TODO: Transitive dependencies! 57 | 58 | condition = f'$([System.Text.RegularExpressions.Regex]::IsMatch($(LibmanUses), ".*(;|^)\s*{ns}/{name}\s*(;|$).*"))' 59 | 60 | resolved_includes = (_resolve(inc) for inc in includes) 61 | inc_prop = f'libman--{ns}__{name}--Include-Path' 62 | ET.SubElement(prop_group, inc_prop).text = ';'.join(resolved_includes) 63 | aid = 'AdditionalIncludeDirectories' 64 | ET.SubElement( 65 | ET.SubElement(autodefs, 'ClCompile', {'Condition': condition}), 66 | aid, 67 | ).text = f'$({inc_prop});%({aid})' 68 | 69 | if lib_path: 70 | link_prop = f'libman--{ns}__{name}--Path' 71 | ET.SubElement(prop_group, link_prop).text = _resolve(lib_path) 72 | adeps = 'AdditionalDependencies' 73 | ET.SubElement( 74 | ET.SubElement(autodefs, 'Link', {'Condition': condition}), 75 | adeps, 76 | ).text = f'$({link_prop});%({adeps})' 77 | 78 | 79 | def _add_pkg(prop_group: ET.Element, autodefs: ET.Element, name: str, path: str, 80 | data: Dict[str, str], out: ConanOutput) -> None: 81 | if os.path.isabs(path): 82 | lmp_content = Path(path).read_text() 83 | else: 84 | lmp_content = data[path] 85 | 86 | NS_RE = re.compile(r'Namespace:\s+(?P.*)$') 87 | namespace: str = '' 88 | LIB_RE = re.compile(r'Library:\s+(?P.*)$') 89 | for line_ in lmp_content.splitlines(): 90 | line: str = line_.strip() 91 | mat = NS_RE.match(line) 92 | if mat: 93 | namespace = mat.group('namespace') 94 | continue 95 | mat = LIB_RE.match(line) 96 | if mat: 97 | _add_library(prop_group, autodefs, namespace, Path(path), 98 | Path(mat.group('path')), data, out) 99 | 100 | 101 | def generate_msbuild_props(data: Dict[str, str], cf: ConanFile) -> str: 102 | out = cf.output 103 | root = ET.Element('Project', {'InitialTargets': 'LibmanValidate'}) 104 | root.set('xmlns', 'http://schemas.microsoft.com/developer/msbuild/2003') 105 | out.info('Generating LibMan import properties for MSbuild') 106 | 107 | check_target = ET.SubElement(root, 'Target', { 108 | 'Name': 'LibmanValidate', 109 | 'Condition': "'$(LibmanDisableValidate)' == ''", 110 | }) 111 | bt = str(cf.settings.build_type) 112 | ET.SubElement( 113 | check_target, 'Error', { 114 | 'Text': 115 | f'The current build configuration `$(Configuration)` does not match ' 116 | f'the configuration installed by Conan (`{bt}`)', 117 | 'Condition': f"'$(Configuration)' != '{bt}'", 118 | }) 119 | vs_platform = { 120 | 'x86': 'Win32', 121 | 'x86_64': 'x64', 122 | }.get(str(cf.settings.arch)) 123 | if vs_platform is not None: 124 | ET.SubElement( 125 | check_target, 'Error', { 126 | 'Text': f'The current build platform `$(Platform)` does not match ' 127 | f'the platform installed by Conan (`{vs_platform}`)', 128 | 'Condition': f"'$(Platform)' != '{vs_platform}'", 129 | }) 130 | 131 | prop_group = ET.SubElement(root, 'PropertyGroup') 132 | ET.SubElement(prop_group, 'LibmanUses', { 133 | 'Condition': "'$(LibmanUses)' == ''", 134 | }) 135 | 136 | autodefs = ET.SubElement(root, 'ItemDefinitionGroup', { 137 | 'Condition': "'$(LibmanDisableAutoUsage)' == ''", 138 | }) 139 | 140 | index_content: str = data['INDEX.lmi'] 141 | PACKAGE_RE = re.compile(r'Package:\s+(?P[^;]+)\s*;\s*(?P.+)$') 142 | for line_ in index_content.splitlines(): 143 | line: str = line_.strip() 144 | mat = PACKAGE_RE.match(line) 145 | if mat is None: 146 | continue 147 | 148 | pkg_name = mat.groupdict()['name'] 149 | lmp_path = mat.groupdict()['path'] 150 | _add_pkg(prop_group, autodefs, pkg_name, lmp_path, data, out) 151 | 152 | dom = minidom.parseString(ET.tostring(root, encoding='UTF-8')) 153 | pretty = dom.toprettyxml(indent=' ', encoding='UTF-8') 154 | return pretty.decode() 155 | -------------------------------------------------------------------------------- /pmm.cmake: -------------------------------------------------------------------------------- 1 | ## MIT License 2 | ## 3 | ## Copyright (c) 2018 vector-of-bool 4 | ## 5 | ## Permission is hereby granted, free of charge, to any person obtaining a copy 6 | ## of this software and associated documentation files (the "Software"), to deal 7 | ## in the Software without restriction, including without limitation the rights 8 | ## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | ## copies of the Software, and to permit persons to whom the Software is 10 | ## furnished to do so, subject to the following conditions: 11 | ## 12 | ## The above copyright notice and this permission notice shall be included in all 13 | ## copies or substantial portions of the Software. 14 | ## 15 | ## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | ## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | ## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | ## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | ## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | ## OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | ## SOFTWARE. 22 | 23 | # Bump this version to change what PMM version is downloaded 24 | set(PMM_VERSION_INIT 1.3.0) 25 | 26 | # Helpful macro to set a variable if it isn't already set 27 | macro(_pmm_set_if_undef varname) 28 | if(NOT DEFINED "${varname}") 29 | set("${varname}" "${ARGN}") 30 | endif() 31 | endmacro() 32 | 33 | ## Variables used by this script 34 | # The version: 35 | _pmm_set_if_undef(PMM_VERSION ${PMM_VERSION_INIT}) 36 | # The base URL we download PMM from: 37 | _pmm_set_if_undef(PMM_URL_BASE "https://vector-of-bool.github.io/pmm") 38 | # The real URL we download from (Based on the version) 39 | _pmm_set_if_undef(PMM_URL "${PMM_URL_BASE}/${PMM_VERSION}") 40 | # The directory where we store our downloaded files 41 | _pmm_set_if_undef(PMM_DIR_BASE "${CMAKE_BINARY_DIR}/_pmm") 42 | _pmm_set_if_undef(PMM_DIR "${PMM_DIR_BASE}/${PMM_VERSION}") 43 | 44 | # The file that we first download 45 | set(_PMM_ENTRY_FILE "${PMM_DIR}/entry.cmake") 46 | 47 | if(NOT EXISTS "${_PMM_ENTRY_FILE}" OR PMM_ALWAYS_DOWNLOAD) 48 | file( 49 | DOWNLOAD "${PMM_URL}/entry.cmake" 50 | "${_PMM_ENTRY_FILE}.tmp" 51 | STATUS pair 52 | ) 53 | list(GET pair 0 rc) 54 | list(GET pair 1 msg) 55 | if(rc) 56 | message(FATAL_ERROR "Failed to download PMM entry file") 57 | endif() 58 | file(RENAME "${_PMM_ENTRY_FILE}.tmp" "${_PMM_ENTRY_FILE}") 59 | endif() 60 | 61 | # ^^^ DO NOT CHANGE THIS LINE vvv 62 | set(_PMM_BOOTSTRAP_VERSION 1) 63 | # ^^^ DO NOT CHANGE THIS LINE ^^^ 64 | 65 | include("${_PMM_ENTRY_FILE}") 66 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library( 2 | libman 3 | lm/lm.hpp 4 | lm/lm.cpp 5 | ) 6 | add_library(lm::libman ALIAS libman) 7 | target_include_directories(libman 8 | PUBLIC $ 9 | ) 10 | set_property(TARGET libman PROPERTY PREFIX "") 11 | -------------------------------------------------------------------------------- /src/lm/lm.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int lm::calculate_value() { 4 | // How many roads must a man walk down? 5 | return 6 * 7; 6 | } 7 | -------------------------------------------------------------------------------- /src/lm/lm.hpp: -------------------------------------------------------------------------------- 1 | #ifndef LM_LM_HPP_INCLUDED 2 | #define LM_LM_HPP_INCLUDED 3 | 4 | namespace lm { 5 | 6 | /** 7 | * Calculate the answer. Not sure what the question is, though... 8 | */ 9 | int calculate_value(); 10 | 11 | } // namespace lm 12 | 13 | #endif // LM_LM_HPP_INCLUDED -------------------------------------------------------------------------------- /test_package/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(LibManTest) 3 | 4 | include(${CMAKE_BINARY_DIR}/libman.cmake) 5 | import_packages(spdlog) 6 | 7 | add_library(something something.cpp) 8 | 9 | # Link the imported dependency. This require fmt::fmt, but libman should have 10 | # created that transitive usage and the compile will succeed 11 | target_link_libraries(something PUBLIC spdlog::spdlog) 12 | 13 | export_package(REQUIRES meow) 14 | export_library(something HEADER_ROOTS src) 15 | -------------------------------------------------------------------------------- /test_package/LibManTest.lmp.in: -------------------------------------------------------------------------------- 1 | # This file was generated by `export_package` from CMake for LibManTest 2 | Type: Package 3 | Name: LibManTest 4 | Namespace: LibManTest 5 | Library: $>,$,$>,$,something>>.lml 6 | Requires: spdlog 7 | Requires: meow 8 | -------------------------------------------------------------------------------- /test_package/conanfile.py: -------------------------------------------------------------------------------- 1 | import conans 2 | 3 | 4 | libman = conans.python_requires('libman/0.2.0@test/test') 5 | 6 | 7 | class ConanFile(libman.CMakeConanFile): 8 | generators = 'LibMan' 9 | libman_for = 'cmake' 10 | requires = ( 11 | 'spdlog/[*]@bincrafters/stable', 12 | ) 13 | 14 | def test(self): 15 | pass 16 | -------------------------------------------------------------------------------- /test_package/something.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int meow() {} -------------------------------------------------------------------------------- /test_package/something.lml.in: -------------------------------------------------------------------------------- 1 | Type: Library 2 | Name: $>,$,$>,$,something>> 3 | Include-Path: include 4 | Path: lib/$ 5 | $<$>:Preprocessor-Define: $, 6 | Preprocessor-Define: > 7 | >Uses: spdlog/spdlog 8 | -------------------------------------------------------------------------------- /test_package/src/foo/bar.hpp: -------------------------------------------------------------------------------- 1 | #ifndef FOO_BAR_HPP_INCLUDED 2 | #define FOO_BAR_HPP_INCLUDED 3 | 4 | // This is a test header 5 | 6 | #endif // FOO_BAR_HPP_INCLUDED -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(Catch) 2 | 3 | add_library(lm-tests-base STATIC test-main.cpp) 4 | target_link_libraries(lm-tests-base PUBLIC Catch2::Catch2) 5 | 6 | set(test_dirs 7 | just-include 8 | import-empty 9 | import-simple 10 | export-simple 11 | ) 12 | 13 | foreach(test_dir IN LISTS test_dirs) 14 | get_filename_component(test_dir_abs "${test_dir}" ABSOLUTE) 15 | set(test_dir_bin "${CMAKE_CURRENT_BINARY_DIR}/build-${test_dir}") 16 | add_test( 17 | NAME "libman.project.${test_dir}" 18 | COMMAND "${CMAKE_COMMAND}" 19 | -D LIBMAN_SRC=${PROJECT_SOURCE_DIR} 20 | -D LIBMAN_BIN=${PROJECT_BINARY_DIR} 21 | -D TEST_SRC_DIR=${test_dir_abs} 22 | -D TEST_BIN_DIR=${test_dir_bin} 23 | -D GENERATOR=${CMAKE_GENERATOR} 24 | -D CONFIG=$ 25 | -P "${CMAKE_CURRENT_SOURCE_DIR}/RunProjectTest.cmake" 26 | ) 27 | endforeach() 28 | 29 | add_test( 30 | NAME libman.export-then-import 31 | COMMAND "${CMAKE_COMMAND}" 32 | -D LIBMAN_SRC=${PROJECT_SOURCE_DIR} 33 | -D LIBMAN_BIN=${PROJECT_BINARY_DIR} 34 | -D GENERATOR=${CMAKE_GENERATOR} 35 | -D CONFIG=$ 36 | -P "${CMAKE_CURRENT_SOURCE_DIR}/import-export/RunTest.cmake" 37 | ) 38 | -------------------------------------------------------------------------------- /tests/RunProjectTest.cmake: -------------------------------------------------------------------------------- 1 | file(REMOVE_RECURSE "${TEST_BIN_DIR}") 2 | execute_process( 3 | COMMAND "${CMAKE_COMMAND}" 4 | -G "${GENERATOR}" 5 | -D LIBMAN_INCLUDE=${LIBMAN_SRC}/cmake/libman.cmake 6 | -D CMAKE_BUILD_TYPE=${CONFIG} 7 | -S "${TEST_SRC_DIR}" 8 | -B "${TEST_BIN_DIR}" 9 | RESULT_VARIABLE retc 10 | ) 11 | 12 | if(retc) 13 | message(FATAL_ERROR "Configure failed [${retc}]") 14 | endif() 15 | 16 | execute_process( 17 | COMMAND "${CMAKE_COMMAND}" 18 | --build ${TEST_BIN_DIR} 19 | --config ${CONFIG} 20 | RESULT_VARIABLE retc 21 | ) 22 | 23 | if(retc) 24 | message(FATAL_ERROR "Build failed [${retc}]") 25 | endif() 26 | -------------------------------------------------------------------------------- /tests/export-simple/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | project(TestProject) 3 | 4 | include(${LIBMAN_INCLUDE}) 5 | 6 | add_library(some-library 7 | src/simple.cpp 8 | src/simple.hpp 9 | ) 10 | 11 | export_package(ADD_TO_ALL) 12 | 13 | export_library(some-library 14 | HEADER_ROOTS src 15 | ) 16 | -------------------------------------------------------------------------------- /tests/export-simple/src/simple.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/libman/088ea18796dc664260a5b2dcf4d92a83b08188c6/tests/export-simple/src/simple.cpp -------------------------------------------------------------------------------- /tests/export-simple/src/simple.hpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/libman/088ea18796dc664260a5b2dcf4d92a83b08188c6/tests/export-simple/src/simple.hpp -------------------------------------------------------------------------------- /tests/import-empty/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | project(ImportEmpty) 3 | 4 | include(${LIBMAN_INCLUDE}) 5 | import_packages() 6 | -------------------------------------------------------------------------------- /tests/import-empty/INDEX.lmi: -------------------------------------------------------------------------------- 1 | Type: Index -------------------------------------------------------------------------------- /tests/import-export/RunTest.cmake: -------------------------------------------------------------------------------- 1 | set(BINDIR_BASE "${LIBMAN_BIN}/test-import-export") 2 | 3 | function(build_project name dir) 4 | get_filename_component(dir "${CMAKE_CURRENT_LIST_DIR}/${dir}" ABSOLUTE) 5 | get_filename_component(bindir "${BINDIR_BASE}/${name}/build" ABSOLUTE) 6 | file(REMOVE_RECURSE "${bindir}") 7 | execute_process( 8 | COMMAND "${CMAKE_COMMAND}" 9 | -G "${GENERATOR}" 10 | -D LIBMAN_INCLUDE=${LIBMAN_SRC}/cmake/libman.cmake 11 | -D CMAKE_BUILD_TYPE=${CONFIG} 12 | ${ARGN} 13 | -S "${dir}" 14 | -B "${bindir}" 15 | RESULT_VARIABLE retc 16 | ) 17 | 18 | if(retc) 19 | message(FATAL_ERROR "Configure failed [${retc}]") 20 | endif() 21 | 22 | execute_process( 23 | COMMAND "${CMAKE_COMMAND}" 24 | --build ${bindir} 25 | --config ${CONFIG} 26 | RESULT_VARIABLE retc 27 | ) 28 | 29 | if(retc) 30 | message(FATAL_ERROR "Build failed [${retc}]") 31 | endif() 32 | endfunction() 33 | 34 | build_project(say-hello-library library) 35 | 36 | string(CONFIGURE [[ 37 | Type: Index 38 | 39 | Package: HelloLibrary; @BINDIR_BASE@/say-hello-library/build/HelloLibrary.libman-export/HelloLibrary.lmp 40 | ]] lmi @ONLY) 41 | 42 | set(gen_index "${BINDIR_BASE}/test-index.lmi") 43 | file(WRITE "${gen_index}" "${lmi}") 44 | 45 | build_project(say-hello-app app -D LIBMAN_INDEX=${gen_index}) 46 | -------------------------------------------------------------------------------- /tests/import-export/app/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | project(SimpleApplication) 3 | 4 | include(${LIBMAN_INCLUDE}) 5 | import_packages(HelloLibrary) 6 | 7 | add_executable(say-hello main.cpp) 8 | target_link_libraries(say-hello PRIVATE say::hello) 9 | -------------------------------------------------------------------------------- /tests/import-export/app/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { say::hello(); } -------------------------------------------------------------------------------- /tests/import-export/library/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | project(HelloLibrary) 3 | 4 | include(${LIBMAN_INCLUDE}) 5 | 6 | add_library(say-hello) 7 | file(GLOB_RECURSE sources src/*) 8 | target_sources(say-hello PRIVATE ${sources}) 9 | set_property(TARGET say-hello PROPERTY LIBMAN_NAME hello) 10 | 11 | export_package(ADD_TO_ALL NAMESPACE say) 12 | export_library(say-hello HEADER_ROOTS src) 13 | -------------------------------------------------------------------------------- /tests/import-export/library/src/say/hello.cpp: -------------------------------------------------------------------------------- 1 | #include "./hello.hpp" 2 | 3 | #include 4 | 5 | void say::hello() { std::cout << "Hello, world!\n"; } -------------------------------------------------------------------------------- /tests/import-export/library/src/say/hello.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SAY_HELLO_HPP_INCLUDED 2 | #define SAY_HELLO_HPP_INCLUDED 3 | 4 | namespace say { 5 | 6 | void hello(); 7 | 8 | } // namespace say 9 | 10 | #endif // SAY_HELLO_HPP_INCLUDED -------------------------------------------------------------------------------- /tests/import-simple/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | project(ImportSimple) 3 | 4 | include(${LIBMAN_INCLUDE}) 5 | import_packages(SimplePackage) 6 | 7 | add_executable(test-exe main.cpp) 8 | 9 | target_link_libraries(test-exe PRIVATE SuperSimple::Meow) 10 | 11 | get_target_property(prop_val SuperSimple::Meow _DUMMY_PROP) 12 | if(NOT prop_val STREQUAL "12") 13 | message(FATAL_ERROR "Loading property value failed") 14 | endif() 15 | -------------------------------------------------------------------------------- /tests/import-simple/INDEX.lmi: -------------------------------------------------------------------------------- 1 | Type: Index 2 | 3 | Package: SimplePackage; subdir/simple.lmp -------------------------------------------------------------------------------- /tests/import-simple/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | return 42 - calculate_answer(); 5 | } -------------------------------------------------------------------------------- /tests/import-simple/subdir/header-dir/super-simple-lib.hpp: -------------------------------------------------------------------------------- 1 | #ifndef DUMMY_EXAMPLE_HPP_INCLUDED 2 | #define DUMMY_EXAMPLE_HPP_INCLUDED 3 | 4 | inline int calculate_answer() { return 42; } 5 | 6 | #endif // DUMMY_EXAMPLE_HPP_INCLUDED -------------------------------------------------------------------------------- /tests/import-simple/subdir/simple-lib.lml: -------------------------------------------------------------------------------- 1 | Type: Library 2 | 3 | Name: Meow 4 | Include-Path: header-dir 5 | 6 | X-CMake-Property: _DUMMY_PROP := 12 7 | -------------------------------------------------------------------------------- /tests/import-simple/subdir/simple.lmp: -------------------------------------------------------------------------------- 1 | Type: Package 2 | 3 | Name: SimplePackage 4 | Namespace: SuperSimple 5 | 6 | Library: simple-lib.lml 7 | -------------------------------------------------------------------------------- /tests/just-include/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | project(TestProject) 3 | 4 | include(${LIBMAN_INCLUDE}) -------------------------------------------------------------------------------- /tests/my_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | int main() { 6 | const auto value = lm::calculate_value(); 7 | if (value == 42) { 8 | std::cout << "We calculated the value correctly\n"; 9 | return 0; 10 | } else { 11 | std::cout << "The value was incorrect!\n"; 12 | return 1; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/test-main.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_MAIN 1 2 | #include --------------------------------------------------------------------------------