├── .github └── workflows │ └── python_ci.yml ├── .gitignore ├── .pylintrc ├── README.md ├── cmake_project_creator ├── __init__.py ├── __main__.py ├── dependency.py ├── directory.py ├── directory_factory.py ├── include_directory.py ├── project_creator.py ├── source_directory.py └── test_directory.py ├── create_cmake_project.py ├── examples ├── dual.json ├── nested_dual.json ├── single.json ├── single_catch2.json ├── single_with_boost.json └── single_with_compiler_options.json └── tests ├── __init__.py ├── test_dependency.py ├── test_directory.py ├── test_directory_factory.py ├── test_include_directory.py ├── test_project_creator.py ├── test_source_directory.py └── test_test_directory.py /.github/workflows/python_ci.yml: -------------------------------------------------------------------------------- 1 | name: PythonCi 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Python 3.8 13 | uses: actions/setup-python@v1 14 | with: 15 | python-version: 3.8 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install pylint 20 | pip install nose 21 | pip install coverage 22 | - name: Running nosetests 23 | run: | 24 | nosetests --with-coverage --cover-erase --cover-min-percentage=70 25 | - name: Analysing the code with pylint 26 | run: | 27 | pylint --rcfile=.pylintrc cmake_project_creator 28 | 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .coverage 3 | cover/ 4 | __pycache__/ 5 | *.pyc 6 | generated_projects/ 7 | -------------------------------------------------------------------------------- /.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 | # Specify a score threshold to be exceeded before program exits with error. 9 | fail-under=9.0 10 | 11 | # Add files or directories to the blacklist. They should be base names, not 12 | # paths. 13 | ignore=CVS 14 | 15 | # Add files or directories matching the regex patterns to the blacklist. The 16 | # regex matches against base names, not paths. 17 | ignore-patterns= 18 | 19 | # Python code to execute, usually for sys.path manipulation such as 20 | # pygtk.require(). 21 | #init-hook= 22 | 23 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 24 | # number of processors available to use. 25 | jobs=1 26 | 27 | # Control the amount of potential inferred values when inferring a single 28 | # object. This can help the performance when dealing with large functions or 29 | # complex, nested conditions. 30 | limit-inference-results=100 31 | 32 | # List of plugins (as comma separated values of python module names) to load, 33 | # usually to register additional checkers. 34 | load-plugins= 35 | 36 | # Pickle collected data for later comparisons. 37 | persistent=yes 38 | 39 | # When enabled, pylint would attempt to guess common misconfiguration and emit 40 | # user-friendly hints instead of false-positive error messages. 41 | suggestion-mode=yes 42 | 43 | # Allow loading of arbitrary C extensions. Extensions are imported into the 44 | # active Python interpreter and may run arbitrary code. 45 | unsafe-load-any-extension=no 46 | 47 | 48 | [MESSAGES CONTROL] 49 | 50 | # Only show warnings with the listed confidence levels. Leave empty to show 51 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 52 | confidence= 53 | 54 | # Disable the message, report, category or checker with the given id(s). You 55 | # can either give multiple identifiers separated by comma (,) or put this 56 | # option multiple times (only on the command line, not in the configuration 57 | # file where it should appear only once). You can also use "--disable=all" to 58 | # disable everything first and then reenable specific checks. For example, if 59 | # you want to run only the similarities checker, you can use "--disable=all 60 | # --enable=similarities". If you want to run only the classes checker, but have 61 | # no Warning level messages displayed, use "--disable=all --enable=classes 62 | # --disable=W". 63 | disable=print-statement, 64 | parameter-unpacking, 65 | unpacking-in-except, 66 | old-raise-syntax, 67 | backtick, 68 | long-suffix, 69 | old-ne-operator, 70 | old-octal-literal, 71 | import-star-module-level, 72 | non-ascii-bytes-literal, 73 | raw-checker-failed, 74 | bad-inline-option, 75 | locally-disabled, 76 | file-ignored, 77 | suppressed-message, 78 | useless-suppression, 79 | deprecated-pragma, 80 | use-symbolic-message-instead, 81 | apply-builtin, 82 | basestring-builtin, 83 | buffer-builtin, 84 | cmp-builtin, 85 | coerce-builtin, 86 | execfile-builtin, 87 | file-builtin, 88 | long-builtin, 89 | raw_input-builtin, 90 | reduce-builtin, 91 | standarderror-builtin, 92 | unicode-builtin, 93 | xrange-builtin, 94 | coerce-method, 95 | delslice-method, 96 | getslice-method, 97 | setslice-method, 98 | no-absolute-import, 99 | old-division, 100 | dict-iter-method, 101 | dict-view-method, 102 | next-method-called, 103 | metaclass-assignment, 104 | indexing-exception, 105 | raising-string, 106 | reload-builtin, 107 | oct-method, 108 | hex-method, 109 | nonzero-method, 110 | cmp-method, 111 | input-builtin, 112 | round-builtin, 113 | intern-builtin, 114 | unichr-builtin, 115 | map-builtin-not-iterating, 116 | zip-builtin-not-iterating, 117 | range-builtin-not-iterating, 118 | filter-builtin-not-iterating, 119 | using-cmp-argument, 120 | eq-without-hash, 121 | div-method, 122 | idiv-method, 123 | rdiv-method, 124 | exception-message-attribute, 125 | invalid-str-codec, 126 | sys-max-int, 127 | bad-python3-import, 128 | deprecated-string-function, 129 | deprecated-str-translate-call, 130 | deprecated-itertools-function, 131 | deprecated-types-field, 132 | next-method-defined, 133 | dict-items-not-iterating, 134 | dict-keys-not-iterating, 135 | dict-values-not-iterating, 136 | deprecated-operator-function, 137 | deprecated-urllib-function, 138 | xreadlines-attribute, 139 | deprecated-sys-function, 140 | exception-escape, 141 | comprehension-escape, 142 | ## Disabled manually 143 | missing-function-docstring, 144 | missing-module-docstring, 145 | missing-class-docstring, 146 | 147 | # Enable the message, report, category or checker with the given id(s). You can 148 | # either give multiple identifier separated by comma (,) or put this option 149 | # multiple time (only on the command line, not in the configuration file where 150 | # it should appear only once). See also the "--disable" option for examples. 151 | enable=c-extension-no-member 152 | 153 | 154 | [REPORTS] 155 | 156 | # Python expression which should return a score less than or equal to 10. You 157 | # have access to the variables 'error', 'warning', 'refactor', and 'convention' 158 | # which contain the number of messages in each category, as well as 'statement' 159 | # which is the total number of statements analyzed. This score is used by the 160 | # global evaluation report (RP0004). 161 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 162 | 163 | # Template used to display messages. This is a python new-style format string 164 | # used to format the message information. See doc for all details. 165 | #msg-template= 166 | 167 | # Set the output format. Available formats are text, parseable, colorized, json 168 | # and msvs (visual studio). You can also give a reporter class, e.g. 169 | # mypackage.mymodule.MyReporterClass. 170 | output-format=text 171 | 172 | # Tells whether to display a full report or only the messages. 173 | reports=no 174 | 175 | # Activate the evaluation score. 176 | score=yes 177 | 178 | 179 | [REFACTORING] 180 | 181 | # Maximum number of nested blocks for function / method body 182 | max-nested-blocks=5 183 | 184 | # Complete name of functions that never returns. When checking for 185 | # inconsistent-return-statements if a never returning function is called then 186 | # it will be considered as an explicit return statement and no message will be 187 | # printed. 188 | never-returning-functions=sys.exit 189 | 190 | 191 | [VARIABLES] 192 | 193 | # List of additional names supposed to be defined in builtins. Remember that 194 | # you should avoid defining new builtins when possible. 195 | additional-builtins= 196 | 197 | # Tells whether unused global variables should be treated as a violation. 198 | allow-global-unused-variables=yes 199 | 200 | # List of strings which can identify a callback function by name. A callback 201 | # name must start or end with one of those strings. 202 | callbacks=cb_, 203 | _cb 204 | 205 | # A regular expression matching the name of dummy variables (i.e. expected to 206 | # not be used). 207 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 208 | 209 | # Argument names that match this expression will be ignored. Default to name 210 | # with leading underscore. 211 | ignored-argument-names=_.*|^ignored_|^unused_ 212 | 213 | # Tells whether we should check for unused import in __init__ files. 214 | init-import=no 215 | 216 | # List of qualified module names which can have objects that can redefine 217 | # builtins. 218 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 219 | 220 | 221 | [FORMAT] 222 | 223 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 224 | expected-line-ending-format= 225 | 226 | # Regexp for a line that is allowed to be longer than the limit. 227 | ignore-long-lines=^\s*(# )??$ 228 | 229 | # Number of spaces of indent required inside a hanging or continued line. 230 | indent-after-paren=4 231 | 232 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 233 | # tab). 234 | indent-string=' ' 235 | 236 | # Maximum number of characters on a single line. 237 | max-line-length=100 238 | 239 | # Maximum number of lines in a module. 240 | max-module-lines=1000 241 | 242 | # Allow the body of a class to be on the same line as the declaration if body 243 | # contains single statement. 244 | single-line-class-stmt=no 245 | 246 | # Allow the body of an if to be on the same line as the test if there is no 247 | # else. 248 | single-line-if-stmt=no 249 | 250 | 251 | [LOGGING] 252 | 253 | # The type of string formatting that logging methods do. `old` means using % 254 | # formatting, `new` is for `{}` formatting. 255 | logging-format-style=old 256 | 257 | # Logging modules to check that the string format arguments are in logging 258 | # function parameter format. 259 | logging-modules=logging 260 | 261 | 262 | [TYPECHECK] 263 | 264 | # List of decorators that produce context managers, such as 265 | # contextlib.contextmanager. Add to this list to register other decorators that 266 | # produce valid context managers. 267 | contextmanager-decorators=contextlib.contextmanager 268 | 269 | # List of members which are set dynamically and missed by pylint inference 270 | # system, and so shouldn't trigger E1101 when accessed. Python regular 271 | # expressions are accepted. 272 | generated-members= 273 | 274 | # Tells whether missing members accessed in mixin class should be ignored. A 275 | # mixin class is detected if its name ends with "mixin" (case insensitive). 276 | ignore-mixin-members=yes 277 | 278 | # Tells whether to warn about missing members when the owner of the attribute 279 | # is inferred to be None. 280 | ignore-none=yes 281 | 282 | # This flag controls whether pylint should warn about no-member and similar 283 | # checks whenever an opaque object is returned when inferring. The inference 284 | # can return multiple potential results while evaluating a Python object, but 285 | # some branches might not be evaluated, which results in partial inference. In 286 | # that case, it might be useful to still emit no-member and other checks for 287 | # the rest of the inferred objects. 288 | ignore-on-opaque-inference=yes 289 | 290 | # List of class names for which member attributes should not be checked (useful 291 | # for classes with dynamically set attributes). This supports the use of 292 | # qualified names. 293 | ignored-classes=optparse.Values,thread._local,_thread._local 294 | 295 | # List of module names for which member attributes should not be checked 296 | # (useful for modules/projects where namespaces are manipulated during runtime 297 | # and thus existing member attributes cannot be deduced by static analysis). It 298 | # supports qualified module names, as well as Unix pattern matching. 299 | ignored-modules= 300 | 301 | # Show a hint with possible names when a member name was not found. The aspect 302 | # of finding the hint is based on edit distance. 303 | missing-member-hint=yes 304 | 305 | # The minimum edit distance a name should have in order to be considered a 306 | # similar match for a missing member name. 307 | missing-member-hint-distance=1 308 | 309 | # The total number of similar names that should be taken in consideration when 310 | # showing a hint for a missing member. 311 | missing-member-max-choices=1 312 | 313 | # List of decorators that change the signature of a decorated function. 314 | signature-mutators= 315 | 316 | 317 | [MISCELLANEOUS] 318 | 319 | # List of note tags to take in consideration, separated by a comma. 320 | notes=FIXME, 321 | XXX, 322 | TODO 323 | 324 | # Regular expression of note tags to take in consideration. 325 | #notes-rgx= 326 | 327 | 328 | [SPELLING] 329 | 330 | # Limits count of emitted suggestions for spelling mistakes. 331 | max-spelling-suggestions=4 332 | 333 | # Spelling dictionary name. Available dictionaries: none. To make it work, 334 | # install the python-enchant package. 335 | spelling-dict= 336 | 337 | # List of comma separated words that should not be checked. 338 | spelling-ignore-words= 339 | 340 | # A path to a file that contains the private dictionary; one word per line. 341 | spelling-private-dict-file= 342 | 343 | # Tells whether to store unknown words to the private dictionary (see the 344 | # --spelling-private-dict-file option) instead of raising a message. 345 | spelling-store-unknown-words=no 346 | 347 | 348 | [BASIC] 349 | 350 | # Naming style matching correct argument names. 351 | argument-naming-style=snake_case 352 | 353 | # Regular expression matching correct argument names. Overrides argument- 354 | # naming-style. 355 | #argument-rgx= 356 | 357 | # Naming style matching correct attribute names. 358 | attr-naming-style=snake_case 359 | 360 | # Regular expression matching correct attribute names. Overrides attr-naming- 361 | # style. 362 | #attr-rgx= 363 | 364 | # Bad variable names which should always be refused, separated by a comma. 365 | bad-names=foo, 366 | bar, 367 | baz, 368 | toto, 369 | tutu, 370 | tata 371 | 372 | # Bad variable names regexes, separated by a comma. If names match any regex, 373 | # they will always be refused 374 | bad-names-rgxs= 375 | 376 | # Naming style matching correct class attribute names. 377 | class-attribute-naming-style=any 378 | 379 | # Regular expression matching correct class attribute names. Overrides class- 380 | # attribute-naming-style. 381 | #class-attribute-rgx= 382 | 383 | # Naming style matching correct class names. 384 | class-naming-style=PascalCase 385 | 386 | # Regular expression matching correct class names. Overrides class-naming- 387 | # style. 388 | #class-rgx= 389 | 390 | # Naming style matching correct constant names. 391 | const-naming-style=UPPER_CASE 392 | 393 | # Regular expression matching correct constant names. Overrides const-naming- 394 | # style. 395 | #const-rgx= 396 | 397 | # Minimum line length for functions/classes that require docstrings, shorter 398 | # ones are exempt. 399 | docstring-min-length=-1 400 | 401 | # Naming style matching correct function names. 402 | function-naming-style=snake_case 403 | 404 | # Regular expression matching correct function names. Overrides function- 405 | # naming-style. 406 | #function-rgx= 407 | 408 | # Good variable names which should always be accepted, separated by a comma. 409 | good-names=i, 410 | j, 411 | k, 412 | ex, 413 | Run, 414 | _ 415 | 416 | # Good variable names regexes, separated by a comma. If names match any regex, 417 | # they will always be accepted 418 | good-names-rgxs= 419 | 420 | # Include a hint for the correct naming format with invalid-name. 421 | include-naming-hint=no 422 | 423 | # Naming style matching correct inline iteration names. 424 | inlinevar-naming-style=any 425 | 426 | # Regular expression matching correct inline iteration names. Overrides 427 | # inlinevar-naming-style. 428 | #inlinevar-rgx= 429 | 430 | # Naming style matching correct method names. 431 | method-naming-style=snake_case 432 | 433 | # Regular expression matching correct method names. Overrides method-naming- 434 | # style. 435 | #method-rgx= 436 | 437 | # Naming style matching correct module names. 438 | module-naming-style=snake_case 439 | 440 | # Regular expression matching correct module names. Overrides module-naming- 441 | # style. 442 | #module-rgx= 443 | 444 | # Colon-delimited sets of names that determine each other's naming style when 445 | # the name regexes allow several styles. 446 | name-group= 447 | 448 | # Regular expression which should only match function or class names that do 449 | # not require a docstring. 450 | no-docstring-rgx=^_ 451 | 452 | # List of decorators that produce properties, such as abc.abstractproperty. Add 453 | # to this list to register other decorators that produce valid properties. 454 | # These decorators are taken in consideration only for invalid-name. 455 | property-classes=abc.abstractproperty 456 | 457 | # Naming style matching correct variable names. 458 | variable-naming-style=snake_case 459 | 460 | # Regular expression matching correct variable names. Overrides variable- 461 | # naming-style. 462 | #variable-rgx= 463 | 464 | 465 | [SIMILARITIES] 466 | 467 | # Ignore comments when computing similarities. 468 | ignore-comments=yes 469 | 470 | # Ignore docstrings when computing similarities. 471 | ignore-docstrings=yes 472 | 473 | # Ignore imports when computing similarities. 474 | ignore-imports=no 475 | 476 | # Minimum lines number of a similarity. 477 | min-similarity-lines=4 478 | 479 | 480 | [STRING] 481 | 482 | # This flag controls whether inconsistent-quotes generates a warning when the 483 | # character used as a quote delimiter is used inconsistently within a module. 484 | check-quote-consistency=no 485 | 486 | # This flag controls whether the implicit-str-concat should generate a warning 487 | # on implicit string concatenation in sequences defined over several lines. 488 | check-str-concat-over-line-jumps=no 489 | 490 | 491 | [CLASSES] 492 | 493 | # List of method names used to declare (i.e. assign) instance attributes. 494 | defining-attr-methods=__init__, 495 | __new__, 496 | setUp, 497 | __post_init__ 498 | 499 | # List of member names, which should be excluded from the protected access 500 | # warning. 501 | exclude-protected=_asdict, 502 | _fields, 503 | _replace, 504 | _source, 505 | _make 506 | 507 | # List of valid names for the first argument in a class method. 508 | valid-classmethod-first-arg=cls 509 | 510 | # List of valid names for the first argument in a metaclass class method. 511 | valid-metaclass-classmethod-first-arg=cls 512 | 513 | 514 | [DESIGN] 515 | 516 | # Maximum number of arguments for function / method. 517 | max-args=5 518 | 519 | # Maximum number of attributes for a class (see R0902). 520 | max-attributes=7 521 | 522 | # Maximum number of boolean expressions in an if statement (see R0916). 523 | max-bool-expr=5 524 | 525 | # Maximum number of branch for function / method body. 526 | max-branches=12 527 | 528 | # Maximum number of locals for function / method body. 529 | max-locals=15 530 | 531 | # Maximum number of parents for a class (see R0901). 532 | max-parents=7 533 | 534 | # Maximum number of public methods for a class (see R0904). 535 | max-public-methods=20 536 | 537 | # Maximum number of return / yield for function / method body. 538 | max-returns=6 539 | 540 | # Maximum number of statements in function / method body. 541 | max-statements=50 542 | 543 | # Minimum number of public methods for a class (see R0903). 544 | min-public-methods=2 545 | 546 | 547 | [IMPORTS] 548 | 549 | # List of modules that can be imported at any level, not just the top level 550 | # one. 551 | allow-any-import-level= 552 | 553 | # Allow wildcard imports from modules that define __all__. 554 | allow-wildcard-with-all=no 555 | 556 | # Analyse import fallback blocks. This can be used to support both Python 2 and 557 | # 3 compatible code, which means that the block might have code that exists 558 | # only in one or another interpreter, leading to false positives when analysed. 559 | analyse-fallback-blocks=no 560 | 561 | # Deprecated modules which should not be used, separated by a comma. 562 | deprecated-modules=optparse,tkinter.tix 563 | 564 | # Create a graph of external dependencies in the given file (report RP0402 must 565 | # not be disabled). 566 | ext-import-graph= 567 | 568 | # Create a graph of every (i.e. internal and external) dependencies in the 569 | # given file (report RP0402 must not be disabled). 570 | import-graph= 571 | 572 | # Create a graph of internal dependencies in the given file (report RP0402 must 573 | # not be disabled). 574 | int-import-graph= 575 | 576 | # Force import order to recognize a module as part of the standard 577 | # compatibility libraries. 578 | known-standard-library= 579 | 580 | # Force import order to recognize a module as part of a third party library. 581 | known-third-party=enchant 582 | 583 | # Couples of modules and preferred modules, separated by a comma. 584 | preferred-modules= 585 | 586 | 587 | [EXCEPTIONS] 588 | 589 | # Exceptions that will emit a warning when being caught. Defaults to 590 | # "BaseException, Exception". 591 | overgeneral-exceptions=BaseException, 592 | Exception 593 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Actions Status](https://github.com/sandordargo/cmake-project-creator/workflows/PythonCi/badge.svg)](https://github.com/sandordargo/cmake-project-creator/actions) 2 | 3 | ## Cmake Project Creator 4 | 5 | Cmake Project Creator helps you generate a new C++ project. Instead of writing the Cmakefiles and creating all the folders by hand, you can either 6 | * generate a project from an already existing description 7 | * write a new description file yourself 8 | 9 | ## Requirements 10 | You need the following software installed on your laptop 11 | - [Python 3.7 or higher](https://www.python.org/downloads/) 12 | - [CMake 3](https://cmake.org/download/) 13 | - [Conan](https://docs.conan.io/en/latest/installation.html) 14 | 15 | ## How to use it? 16 | 17 | Call `./create_cmake_project.py --help` to print the help message. 18 | 19 | Briefly, you can call the `project_creator` with one of the predefined options, or you can pass in your own project description. 20 | 21 | You should also pass in the path with `-o` or `--output` where your new project should be generated. If it's not passed then the project will be created under the generated_projects directory. 22 | 23 | Once a project is generated, navigate to the root of the freshly created project and call `./runTest.sh` to launch the build and the unit tests - if any. Where a test directory is defined, a failing unit test is generated. 24 | 25 | ### Supported unit testing frameworks 26 | 27 | Even though you can include any unit testing framework that is available through Conan, their won't be some default failing tests generated, unless the framework is listed in this section. 28 | 29 | The following unit testing frameworks are supported: 30 | - [catch2](https://github.com/catchorg/Catch2) 31 | - [gtest](https://github.com/google/googletest) 32 | 33 | ## Predefined schemas 34 | 35 | You have the following predefined schemas shipped with `project_creator`. 36 | 37 | ### The `single` directory project with Gtest as a unit testing framework 38 | 39 | Invoke this with `-d examples/single.json`. It will create a project with one include, one source and one test folder. GTest will be included for unit testing through Conan. 40 | 41 | ``` 42 | myProject 43 | |_ include 44 | |_ src 45 | |_ test 46 | 47 | ``` 48 | 49 | ### The `single` directory project with Catch2 as a unit testing framework 50 | 51 | Invoke this with `-d examples/single.json`. It will create a project with one include, one source and one test folder. Catch2 will be included for unit testing through Conan. 52 | 53 | ``` 54 | myProject 55 | |_ include 56 | |_ src 57 | |_ test 58 | 59 | ``` 60 | ### The `single` directory project with compiler options 61 | 62 | Invoke this with `-d examples/single_with_compiler_options.json`. Like the `single.json`, it will create a project with one include, one source and one test folder. More than that, it will define the following compiler options on a project level: `-Wall -Wextra -Wpedantic -Werror`. 63 | 64 | ``` 65 | myProject 66 | |_ include 67 | |_ src 68 | |_ test 69 | 70 | ``` 71 | 72 | ### The `single` directory project with boost 73 | 74 | Invoke this with `-d examples/single_lib.json`. Like the `single.json`, it will create a project with one include, one source and one test folder. More than that, it doesn't only deliver an executable, but it also creates a shared library. 75 | 76 | ``` 77 | myProject 78 | |_ include 79 | |_ src 80 | |_ test 81 | 82 | ``` 83 | 84 | ### The `single` directory project with boost 85 | 86 | Invoke this with `-d examples/single_with_boost.json`. Like the `single.json`, it will create a project with one include, one source and one test folder. More than that, it includes `boost` as a dependency for the source directory and `gtest` for tests. 87 | 88 | ``` 89 | myProject 90 | |_ include 91 | |_ src 92 | |_ test 93 | 94 | ``` 95 | 96 | ### The `dual` directory project 97 | 98 | Invoke the tool with `-d examples/dual.json` and the following project will be generated: 99 | 100 | ``` 101 | myProject 102 | |_ executable 103 | |_ include 104 | |_ src 105 | |_ test 106 | |_ library 107 | |_ include 108 | |_ src 109 | |_ test 110 | ``` 111 | 112 | The `executable` sub-directory will include `library` sub-directory as a dependency and will also have a `main.cpp` as such an executable. The `library` is just a static library. 113 | 114 | GTest will be included for unit testing through Conan. 115 | 116 | ### The `nested_dual` directory project 117 | 118 | Invoke the tool with `-d examples/nested_dual.json` and the following project will be generated: 119 | 120 | ``` 121 | myProject 122 | |_ common_root 123 | |_ executable 124 | |_ include 125 | |_ src 126 | |_ test 127 | |_ library 128 | |_ include 129 | |_ src 130 | |_ test 131 | ``` 132 | 133 | The `executable` sub-directory will include `library` sub-directory as a dependency and will also have a `main.cpp` as such an executable. The `library` is just a static library. 134 | 135 | So what is the difference compared to the `dual` project? Not much, it's just that the subdirectories are nested in a new common root. This is more to show you an example, how it is possible. You can check the description in `examples/nested_dual.json`. 136 | 137 | GTest will be included for unit testing with the help of Conan. 138 | 139 | ## How to write new project descriptions? 140 | 141 | First of all, project descriptions are written in JSON format. 142 | 143 | In the root of the JSON, you'll have to specify the `projectName` attribute, which will be used both for the directory name where the project will be created and evidently it will be the name of the Cmake project. 144 | Then you have to specify an array of `directories`. 145 | 146 | ### The root elements 147 | 148 | There are two mandatory elements: 149 | - `projectName` setting the project name 150 | - `directories` describing the directory structure of the project 151 | 152 | The following items are optional: 153 | - `c++Standard` setting the standard to compile against. If missing, it defaults to *17*. Possible values are: *98*, *11*, *14*, *17*, *20*. 154 | - `compilerOptions` setting extra compiler options. This field takes an array of strings, for example: `"compilerOptions": ["-Wall", "-Wextra", "-Wpedantic", "-Werror"]`. 155 | 156 | ### Directory object 157 | 158 | Each object in the `directories` must specify a `name` attribute, a `type` attribute and/or subdirectories. 159 | 160 | #### Mandatory elements 161 | 162 | The `name` attribute defines the directory name. 163 | 164 | The `type` attribute can take the following values: 165 | - `source` indicating that it will contain implementation files 166 | - `header` indicating that it will contain header files 167 | - `test` indicating that it will contain unit test code 168 | - `intermediate` indicating that it will only contain other subdirectories 169 | 170 | In the subdirectories array, you can list the other `directory` objects to be put nested in the given `directory`. 171 | 172 | #### Optional elements 173 | 174 | A directory object can have the following optional elements: 175 | 176 | - `library` indicating whether a given component should be delivered as a library. Only `source` type directories should use this option. Its values can be `null`, `STATIC` or `SHARED`. Beware that if you decide to go with `null`, you want to be able to link it in unittests. I recommend delivering a library if you want to have tests. [More info on the topic here.](https://stackoverflow.com/a/52105591/3238101) 177 | - `executable` indicating whether a given component should have an executable, if set to `true`, a `main.cpp` will be generated. Only `source` type directories should use this option. 178 | - `include` indicating whether a given `source` component has a corresponding `include` counterpart or not. Only `source` type directories should use this option. 179 | - `dependencies` array containing all the dependencies of the given component 180 | 181 | ### Dependency objects 182 | 183 | The `dependency` object is to describe a dependency of the enclosing component. It has 2 mandatory attributes and 1 optional: 184 | 185 | #### Mandatory elements 186 | - `type` indicating whether the dependency is of another directory of the project (`internal`) or if it's an external one. Among external ones, for the time being, only `conan` is supported. 187 | - `name` indicating the name of the dependency, in case of internal ones it should be the relative path of the depended directory including the project root. (E.g. `common_root/executable/library`) 188 | 189 | #### Optional elements 190 | - `version` is optional and has no role for `internal` dependencies, but for `conan` type dependencies it indicates the version of the component we want to include and the format must follow the Conan syntax. You can both use specific versions or [version ranges](https://docs.conan.io/en/latest/versioning/version_ranges.html) 191 | 192 | ## How to contribute? 193 | 194 | In any case, please check the Github issues, maybe there is already an item, already a discussion concerning your idea. If that's not the case, open an issue and let's discuss it there. 195 | 196 | In terms of coding guidelines, I follow the [PEP 8 Style guide for Pyhon](https://www.python.org/dev/peps/pep-0008/). Tests must be provided. 197 | -------------------------------------------------------------------------------- /cmake_project_creator/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandordargo/cmake-project-creator/3c3ec396210be344e071a13cca89c9d07f6882d1/cmake_project_creator/__init__.py -------------------------------------------------------------------------------- /cmake_project_creator/__main__.py: -------------------------------------------------------------------------------- 1 | from . import project_creator 2 | 3 | if __name__ == "__main__": 4 | project_creator.run() 5 | -------------------------------------------------------------------------------- /cmake_project_creator/dependency.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module models a dependency of a CMake module 3 | """ 4 | 5 | 6 | class Dependency: 7 | """ 8 | This class models a dependency of a CMake module 9 | """ 10 | def __init__(self, dependency_type, name, version=None): 11 | self.type = dependency_type 12 | self.name = name 13 | self.version = version 14 | 15 | def __eq__(self, other) -> bool: 16 | if isinstance(other, Dependency): 17 | return self.type == other.type \ 18 | and self.name == other.name \ 19 | and self.version == other.version 20 | return False 21 | 22 | @staticmethod 23 | def make_dependencies(raw_dependencies): 24 | """ 25 | creates a dependency object based on a the dependency descriptor 26 | :param raw_dependencies: the JSON descriptor 27 | :return: a dependency object 28 | """ 29 | dependencies = [] 30 | for raw_dependency in raw_dependencies: 31 | if "type" not in raw_dependency: 32 | raise ValueError(f"{raw_dependency} does not have a type") 33 | if raw_dependency["type"] != "internal" and "name" not in raw_dependency: 34 | raise ValueError(f"{raw_dependency} does not have a name") 35 | if raw_dependency["type"] == "internal": 36 | continue 37 | 38 | dependency_name = raw_dependency["name"] 39 | dependency_type = raw_dependency["type"] 40 | dependency_version = raw_dependency["version"] if "version" in raw_dependency else None 41 | dependencies.append(Dependency(dependency_type, dependency_name, dependency_version)) 42 | return dependencies 43 | -------------------------------------------------------------------------------- /cmake_project_creator/directory.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class Directory: 5 | def __init__(self, output_root, path, description, project_file_name, dependencies=None): 6 | self.output_root = output_root 7 | self.path = path 8 | self.description = description 9 | self.project_file_name = project_file_name 10 | self.dependencies = dependencies 11 | 12 | def __str__(self): 13 | return self.__repr__() 14 | 15 | def __repr__(self): 16 | return f"Directory(path: {self.path}, description:{self.description})" 17 | 18 | def make_dirs(self): 19 | print("Create directory: ", self.path) 20 | os.makedirs(os.path.join(self.output_root, self.path)) 21 | 22 | def get_name_suffix(self): 23 | if "/" in self.path and len(self.path.split('/')) > 2: 24 | tail = self.path.split('/')[-2] 25 | elif "/" in self.path and len(self.path.split('/')) > 1: 26 | tail = self.path.split('/')[-1] 27 | else: 28 | tail = self.path 29 | print(self.path) 30 | print("tail is ", tail) 31 | return tail 32 | 33 | def get_path_without_project_root(self): 34 | return "/".join(self.path.split('/')[1:]) 35 | 36 | def write_file(self, full_path, content): 37 | if full_path and content: 38 | with open(os.path.join(self.output_root, full_path), "w") as file: 39 | file.write(content) 40 | -------------------------------------------------------------------------------- /cmake_project_creator/directory_factory.py: -------------------------------------------------------------------------------- 1 | from cmake_project_creator import test_directory, dependency, include_directory, source_directory 2 | 3 | 4 | def make(project_home, path, description, project_file_name): 5 | mapper = { 6 | "source": source_directory.SourceDirectory, 7 | "include": include_directory.IncludeDirectory, 8 | "tests": test_directory.TestDirectory, 9 | } 10 | 11 | dependencies = dependency.Dependency.make_dependencies(description["dependencies"]) \ 12 | if "dependencies" in description else [] 13 | return mapper[description["type"]](project_home, 14 | path, 15 | description, 16 | project_file_name, 17 | dependencies) 18 | -------------------------------------------------------------------------------- /cmake_project_creator/include_directory.py: -------------------------------------------------------------------------------- 1 | from cmake_project_creator import directory 2 | 3 | 4 | class IncludeDirectory(directory.Directory): 5 | def __init__(self, project_home, path, description, project_file_name, dependencies): 6 | self.path = path 7 | super().__init__(project_home, 8 | path, 9 | description, 10 | project_file_name, 11 | dependencies) 12 | 13 | def create(self, _): 14 | directory.Directory.make_dirs(self) 15 | 16 | self.write_file(*self.create_header_content()) 17 | 18 | def create_header_content(self): 19 | return f'{self.path}/{self.project_file_name}.h', \ 20 | f"""#pragma once 21 | 22 | class {self.project_file_name} {{ 23 | public: 24 | void hello(); 25 | }}; 26 | """ 27 | -------------------------------------------------------------------------------- /cmake_project_creator/project_creator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import os 5 | import shutil 6 | import stat 7 | import json 8 | from cmake_project_creator import directory_factory 9 | 10 | 11 | def parse_arguments(): 12 | parser = argparse.ArgumentParser() 13 | parser.add_argument('-d', '--description', 14 | default="examples/single.json", 15 | type=str, 16 | dest='description', 17 | help='Project description' 18 | ) 19 | parser.add_argument('-n', '--name', 20 | type=str, 21 | dest='name', 22 | help="You can override the project's name that is used " 23 | "as the name of the containing folder and throughout " 24 | "the Cmakelists files" 25 | ) 26 | parser.add_argument('-o', '--output', 27 | default="generated_projects", 28 | type=str, 29 | dest='output_root', 30 | help='The directory where your new projects will be generated. ' 31 | 'In case you want to generate MyCmakeProject and you set ' 32 | '--output to MyProjects, ' 33 | 'MyProjects/MyCmakeProject will be generated. ' 34 | 'In case the directory doesn\'t exist ' 35 | 'it will be still created.' 36 | ) 37 | parser.add_argument('-c', '--cleanup', 38 | action='store_true', 39 | dest='cleanup', 40 | help='Tries to remove target directory before running the script' 41 | ) 42 | arguments = parser.parse_args() 43 | return arguments 44 | 45 | 46 | def create_plumbing(output_root, path, subdirs, parsed_directories): 47 | conan_dependencies = collect_conan_dependencies(parsed_directories.values()) 48 | conan2_dependencies = collect_conan2_dependencies(parsed_directories.values()) 49 | is_conan = len(conan_dependencies) > 0 50 | is_conan2 = len(conan2_dependencies) > 0 51 | if is_conan and is_conan2: 52 | raise ValueError("You cannot use both conan and conan2 dependencies at the same time") 53 | conan_version = 0 54 | if is_conan: 55 | conan_version = 1 56 | elif is_conan2: 57 | conan_version = 2 58 | 59 | with open(f'{os.path.join(output_root, path)}/runTests.sh', "w") as run_tests: 60 | run_tests.write(create_runtest(conan_version, path, subdirs)) 61 | file_stats = os.stat(f'{os.path.join(output_root, path)}/runTests.sh') 62 | os.chmod(f'{os.path.join(output_root, path)}/runTests.sh', file_stats.st_mode | stat.S_IEXEC) 63 | 64 | if is_conan: 65 | with open(f'{os.path.join(output_root, path)}/conanfile.txt', "w") as conanfile: 66 | conanfile.write(create_conanfile(conan_dependencies)) 67 | if is_conan2: 68 | with open(f'{os.path.join(output_root, path)}/conanfile.txt', "w") as conanfile: 69 | conanfile.write(create_conanfile2(conan2_dependencies)) 70 | 71 | def create_runtest(conan_version, path, subdirs): 72 | if conan_version == 1: 73 | conan_install = "conan install . -s compiler.libcxx=libstdc++11" 74 | elif conan_version == 2: 75 | conan_install = "conan install . --output-folder=build --build=missing" 76 | else: 77 | conan_install = "" 78 | 79 | print(subdirs) 80 | 81 | runtest = " ; ".join( 82 | f"./{sd}/{path}_{sd.split('/')[-2] if '/' in sd else sd}_test" 83 | for sd in subdirs if 'test' in sd) 84 | runtest = f"({runtest})" if runtest else "" 85 | 86 | return f"""CURRENT_DIR=`pwd` 87 | rm -rf build && mkdir build && {(conan_install + " &&") if conan_install else ""} \ 88 | cd build && cmake .. {"-DCMAKE_TOOLCHAIN_FILE=build/Release/generators/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release" if conan_version == 2 else ""} && cmake --build . {"&& " + runtest if runtest else ""} 89 | cd "${{CURRENT_DIR}}" 90 | """ 91 | 92 | def create_conanfile(dependencies): 93 | requires = [f"{name}/{version}" for name, version in dependencies.items()] 94 | requires_command = "\n".join(requires) 95 | return f"""[requires] 96 | {requires_command} 97 | 98 | [generators] 99 | cmake 100 | """ 101 | 102 | def create_conanfile2(dependencies): 103 | requires = [f"{name}/{version}" for name, version in dependencies.items()] 104 | requires_command = "\n".join(requires) 105 | return f"""[requires] 106 | {requires_command} 107 | 108 | [generators] 109 | CMakeDeps 110 | CMakeToolchain 111 | 112 | [layout] 113 | cmake_layout 114 | """ 115 | 116 | def collect_cmake_subdirectories(directories, name="", paths=None): 117 | if not paths: 118 | paths = [] 119 | for directory in directories: 120 | if 'type' in directory and directory['type'] in ["source", "tests"]: 121 | paths.append(os.path.join(name, directory['name'])) 122 | if 'subdirectories' in directory: 123 | paths = collect_cmake_subdirectories(directory['subdirectories'], 124 | os.path.join(name, directory['name']), 125 | paths) 126 | return paths 127 | 128 | 129 | def create_main_cmakelists(output_root, project_directory_name, subdirectories, cpp_version=None, compiler_options_list=None): 130 | add_subdirectories_commands = "" 131 | if cpp_version is None: 132 | cpp_version = "17" 133 | if cpp_version not in ["98", "11", "14", "17", "20", "23"]: 134 | raise ValueError(f"{cpp_version} is not among the supported C++ versions") 135 | compiler_options_list = compiler_options_list or [] 136 | 137 | add_compiler_options_command = "" 138 | if compiler_options_list: 139 | add_compiler_options_command = "add_compile_options(" 140 | add_compiler_options_command += " ".join(compiler_options_list) 141 | add_compiler_options_command += ")" 142 | 143 | for subdirectory in subdirectories: 144 | add_subdirectories_commands += f'add_subdirectory("{subdirectory}")' + "\n" 145 | 146 | content = \ 147 | f"""cmake_minimum_required(VERSION 3.10) 148 | project({project_directory_name}) 149 | set(CMAKE_CXX_STANDARD {cpp_version}) 150 | """ 151 | 152 | if add_compiler_options_command.strip(): 153 | content += f"{add_compiler_options_command.strip()}\n" 154 | 155 | content += \ 156 | f""" 157 | {add_subdirectories_commands.strip()} 158 | """ 159 | return f'{os.path.join(output_root, project_directory_name)}/CMakeLists.txt', content 160 | 161 | 162 | def parse_directories(directories, project_home, relative_root, project_file_name): 163 | parsed_directories = {} 164 | for directory in directories: 165 | rel_path = os.path.join(relative_root, directory['name']) 166 | 167 | if directory["type"] == "intermediate": 168 | parsed_directories.update(parse_directories(directory['subdirectories'], 169 | project_home, 170 | rel_path, 171 | project_file_name)) 172 | continue 173 | 174 | parsed_directories[rel_path] = directory_factory.make(project_home, 175 | rel_path, 176 | directory, 177 | project_file_name) 178 | return parsed_directories 179 | 180 | 181 | def create_cpp_project(parsed_directories): 182 | for directory in parsed_directories.values(): 183 | directory.create(parsed_directories) 184 | 185 | 186 | def collect_conan_dependencies(parsed_directories): 187 | conan_dependencies = {} 188 | for directory in parsed_directories: 189 | for dependency in directory.dependencies: 190 | if dependency.type == "conan": 191 | if dependency.name in conan_dependencies: 192 | if dependency.version != conan_dependencies[dependency.name]: 193 | raise ValueError(f"{dependency.name} is referenced with multiple versions") 194 | else: 195 | conan_dependencies[dependency.name] = dependency.version 196 | return conan_dependencies 197 | 198 | def collect_conan2_dependencies(parsed_directories): 199 | conan_dependencies = {} 200 | for directory in parsed_directories: 201 | for dependency in directory.dependencies: 202 | if dependency.type == "conan2": 203 | if dependency.name in conan_dependencies: 204 | if dependency.version != conan_dependencies[dependency.name]: 205 | raise ValueError(f"{dependency.name} is referenced with multiple versions") 206 | else: 207 | conan_dependencies[dependency.name] = dependency.version 208 | return conan_dependencies 209 | 210 | def collect_compiler_options(project_description): 211 | if "compilerOptions" not in project_description: 212 | return [] 213 | return project_description["compilerOptions"] 214 | 215 | def cleanup_project_folder(project_directory): 216 | try: 217 | shutil.rmtree(project_directory) 218 | except Exception as exception: 219 | print(f"Cleanup of {project_directory} failed due to {exception}") 220 | 221 | 222 | def prepare_build_directory(project_directory): 223 | os.makedirs(os.path.join(project_directory, 'build')) 224 | 225 | 226 | def write_file(full_path, content): 227 | if full_path and content: 228 | with open(full_path, "w") as file: 229 | file.write(content) 230 | 231 | 232 | def run(): 233 | arguments = parse_arguments() 234 | 235 | with open(arguments.description) as json_file: 236 | project_description = json.load(json_file) 237 | 238 | output_root = arguments.output_root 239 | 240 | project_dir_name = arguments.name if arguments.name else project_description['projectName'] 241 | project_file_name = project_dir_name.capitalize() 242 | 243 | if arguments.cleanup: 244 | cleanup_project_folder(os.path.join(output_root, project_dir_name)) 245 | 246 | prepare_build_directory(os.path.join(output_root, project_dir_name)) 247 | 248 | cmake_subdirectories = collect_cmake_subdirectories(project_description['directories']) 249 | print(f"The identified sub cmake projects are {cmake_subdirectories}") 250 | 251 | cpp_version = project_description["c++Standard"] \ 252 | if "c++Standard" in project_description else None 253 | write_file(*create_main_cmakelists(output_root, 254 | project_dir_name, 255 | cmake_subdirectories, 256 | cpp_version, 257 | collect_compiler_options(project_description))) 258 | 259 | parsed_directories = parse_directories(project_description['directories'], 260 | output_root, 261 | project_dir_name, 262 | project_file_name) 263 | create_plumbing(output_root, 264 | project_dir_name, 265 | cmake_subdirectories, 266 | parsed_directories) 267 | create_cpp_project(parsed_directories) 268 | 269 | 270 | if __name__ == "__main__": 271 | run() 272 | -------------------------------------------------------------------------------- /cmake_project_creator/source_directory.py: -------------------------------------------------------------------------------- 1 | from cmake_project_creator import directory 2 | 3 | 4 | class SourceDirectory(directory.Directory): 5 | def __init__(self, project_home, path, description, project_file_name, dependencies): 6 | super().__init__(project_home, 7 | path, 8 | description, 9 | project_file_name, 10 | dependencies) 11 | self.path = path 12 | self.has_include = "include" in self.description and self.description["include"] == 'true' 13 | 14 | def __eq__(self, o: object) -> bool: 15 | return super().__eq__(o) and self.path == o.path and self.has_include == o.has_include 16 | 17 | def create(self, parsed_dirs): 18 | directory.Directory.make_dirs(self) 19 | 20 | file_creators = [ 21 | self.create_cmakelists, 22 | self.create_source_file, 23 | self.create_main 24 | ] 25 | 26 | for file_creator in file_creators: 27 | self.write_file(*file_creator(parsed_dirs.values())) 28 | 29 | def create_main(self, _): 30 | if self.description['executable'] == 'true': 31 | content = \ 32 | f"""#include "{self.project_file_name}.{'h' if self.has_include else 'cpp'}" 33 | 34 | int main() {{ 35 | {self.project_file_name} o; 36 | o.hello(); 37 | }} 38 | """ 39 | return f'{self.path}/main.cpp', content 40 | return None, "" 41 | 42 | def create_source_file(self, _): 43 | include_statement = f'#include "{self.project_file_name}.h"' if self.has_include else '' 44 | content = \ 45 | f"""{include_statement} 46 | #include 47 | 48 | void {self.project_file_name}::hello() {{ 49 | std::cout << "hello" << std::endl; 50 | }} 51 | """ 52 | return f'{self.path}/{self.project_file_name}.cpp', content 53 | 54 | def create_cmakelists(self, parsed_dirs): 55 | dependencies = self.description["dependencies"] 56 | 57 | is_conan = any(dependency["type"] == "conan" for dependency in self.description["dependencies"]) 58 | 59 | link_directories_commands = [] 60 | include_dependent_libraries_commands = [] 61 | for dependency in dependencies: 62 | if dependency['type'] == "internal": 63 | print(f"Dependency on {dependency['link']}") 64 | raise_if_invalid_dependency(dependency, parsed_dirs) 65 | 66 | link_directories_commands.extend(self.build_include_directories_for_dependency( 67 | parsed_dirs, dependency)) 68 | link_directories_commands.append( 69 | f"link_directories(${{PROJECT_SOURCE_DIR}}/{dependency['link']})") 70 | 71 | elif dependency['type'] == "conan": 72 | pass 73 | else: 74 | print(f"Dependency on {dependency['link']} has an unsupported type: \ 75 | {dependency['type']}") 76 | 77 | link_directories_command = "\n".join(link_directories_commands) 78 | include_dependent_libraries_command = "\n".join(include_dependent_libraries_commands) 79 | 80 | content = \ 81 | f"""set(BINARY ${{CMAKE_PROJECT_NAME}}_{directory.Directory.get_name_suffix(self)}) 82 | 83 | {"include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)" if is_conan else ""} 84 | {"conan_basic_setup() # Prepares the CMakeList.txt for Conan." if is_conan else ""} 85 | 86 | {link_directories_command} 87 | 88 | {"include_directories(../include)" if self.has_include else ""} 89 | {include_dependent_libraries_command} 90 | 91 | file(GLOB SOURCES *.cpp) 92 | set(SOURCES ${{SOURCES}}) 93 | 94 | {self.build_executable_command()} 95 | {self.build_library_command()} 96 | """ 97 | return f'{self.path}/CMakeLists.txt', content 98 | 99 | def build_executable_command(self): 100 | executable = self.description["executable"] if "executable" in self.description else "false" 101 | if not executable: 102 | return "" 103 | is_custom_name_defined = "executable_name" in self.description 104 | executable_name = self.description["executable_name"] if is_custom_name_defined \ 105 | else f"{self.project_file_name}_{directory.Directory.get_name_suffix(self)}" 106 | 107 | executable_command = f"add_executable({executable_name} ${{SOURCES}})" 108 | return executable_command 109 | 110 | def build_library_command(self): 111 | library = self.description["library"] if "library" in self.description else None 112 | if not library: 113 | return "" 114 | is_custom_executable_name_defined = "executable_name" in self.description 115 | executable_name = self.description["executable_name"] if is_custom_executable_name_defined \ 116 | else f"{self.project_file_name}_{directory.Directory.get_name_suffix(self)}" 117 | 118 | is_custom_name_defined = "library_name" in self.description 119 | library_name = self.description["library_name"] if is_custom_name_defined \ 120 | else f"${{BINARY}}_lib" 121 | 122 | library_command = \ 123 | f"""add_library({library_name} {library.upper()} ${{SOURCES}}) 124 | target_link_libraries({executable_name} ${{BINARY}}_lib)""" 125 | return library_command 126 | 127 | def build_include_directories_for_dependency(self, parsed_dirs, dependency): 128 | return [f"include_directories(${{PROJECT_SOURCE_DIR}}/" \ 129 | f"{get_include_library_of_directory(d)})" 130 | for d in parsed_dirs 131 | if d.description["type"] and 132 | "include" in d.description and 133 | d.description["include"] == "true" and 134 | d.get_path_without_project_root() == dependency['link']] 135 | 136 | 137 | def raise_if_invalid_dependency(dependency, parsed_dirs): 138 | valid_dependency = False 139 | for directory in parsed_dirs: 140 | if directory.description["type"] and \ 141 | directory.description["type"] in ["source", "include"] and \ 142 | directory.get_path_without_project_root() == dependency["link"]: 143 | valid_dependency = True 144 | break 145 | if not valid_dependency: 146 | raise ValueError(f"dependent directory {dependency['link']} doesn't exist") 147 | 148 | 149 | def get_include_library_of_directory(directory): 150 | return "/".join(directory.path.split('/')[1:-1]) + "/include" 151 | -------------------------------------------------------------------------------- /cmake_project_creator/test_directory.py: -------------------------------------------------------------------------------- 1 | """ 2 | This modules represents a CMake test module 3 | """ 4 | from cmake_project_creator import directory 5 | 6 | 7 | class TestDirectory(directory.Directory): 8 | """ 9 | This class creates a test module of a Cmake project 10 | """ 11 | 12 | def __init__(self, project_home, path, description, project_file_name, dependencies): 13 | super().__init__(project_home, 14 | path, 15 | description, 16 | project_file_name, 17 | dependencies) 18 | self.test_framework = [dependency.name for dependency in self.dependencies][0] if self.dependencies else None 19 | 20 | def create(self, parsed_dirs): 21 | """ 22 | Creates all the files needed for a test module 23 | """ 24 | directory.Directory.make_dirs(self) 25 | 26 | file_creators = [ 27 | self.create_cmakelists, 28 | self.create_source_file, 29 | self.create_main 30 | ] 31 | for file_creator in file_creators: 32 | self.write_file(*file_creator(parsed_dirs)) 33 | 34 | def create_cmakelists(self, parsed_dirs): 35 | """ 36 | :return: the path and content of CMakelists.txt in a test module 37 | """ 38 | is_conan = any(dependency.type == "conan" for dependency in self.dependencies) 39 | is_conan2 = any(dependency.type == "conan2" for dependency in self.dependencies) 40 | if is_conan and is_conan2: 41 | raise ValueError(f"Cannot have conan and conan2 dependencies at the same time") 42 | parent_path_to_look_for = self.path[:self.path.rfind('/')] 43 | 44 | is_there_source_dir = any(k.startswith(parent_path_to_look_for) and v.description["type"] == "source" and v.description["library"] is not None 45 | for k, v in parsed_dirs.items()) 46 | 47 | src_lib_to_link = "${CMAKE_PROJECT_NAME}_src_lib" if is_there_source_dir else "" 48 | 49 | tail = directory.Directory.get_name_suffix(self) 50 | 51 | conan_test_lib = "" 52 | if is_conan: 53 | conan_test_lib = "${CONAN_LIBS}" 54 | if is_conan2: 55 | if self.test_framework == "gtest": 56 | conan_test_lib = "gtest::gtest" 57 | test_package = "GTest" 58 | if self.test_framework == "catch2": 59 | conan_test_lib = "Catch2::Catch2WithMain" 60 | test_package = "Catch2" 61 | 62 | return f'{self.path}/CMakeLists.txt', \ 63 | f"""set(BINARY ${{CMAKE_PROJECT_NAME}}_{tail}_test) 64 | file(GLOB_RECURSE TEST_SOURCES LIST_DIRECTORIES false *.h *.cpp) 65 | 66 | set(SOURCES ${{TEST_SOURCES}}) 67 | {f"find_package({test_package} REQUIRED)" if is_conan2 else ""} 68 | {"include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)" if is_conan else ""} 69 | {"conan_basic_setup() # Prepares the CMakeList.txt for Conan." if is_conan else ""} 70 | 71 | include_directories(../include) 72 | 73 | add_executable(${{BINARY}} ${{TEST_SOURCES}}) 74 | {str(f"target_link_libraries(${{BINARY}} PUBLIC {conan_test_lib} {src_lib_to_link}").strip() + ")"} 75 | 76 | add_test(NAME ${{BINARY}} COMMAND ${{BINARY}}) 77 | """ 78 | 79 | def create_source_file(self, _): 80 | """ 81 | :return: the path and content of cpp file in a test module 82 | """ 83 | return { 84 | "gtest": self.create_gtest_source_file, 85 | "catch2": self.create_catch2_source_file, 86 | "unknown": self.create_nothing 87 | }.get(self.test_framework, self.create_nothing)() 88 | 89 | def create_gtest_source_file(self): 90 | return f'{self.path}/Test{self.project_file_name}.cpp', \ 91 | f"""#include 92 | #include "{self.project_file_name}.h" 93 | 94 | TEST(blaTest, test1) {{ 95 | {self.project_file_name} x; 96 | x.hello(); 97 | ASSERT_EQ (1, 0); 98 | }} 99 | """ 100 | 101 | def create_catch2_source_file(self): 102 | return f'{self.path}/Test{self.project_file_name}.cpp', \ 103 | f"""#include 104 | 105 | #include "{self.project_file_name}.h" 106 | 107 | TEST_CASE("blaTest", "test1") {{ 108 | {self.project_file_name} x; 109 | x.hello(); 110 | REQUIRE( 1 == 0 ); 111 | }} 112 | """ 113 | 114 | def create_main(self, _): 115 | """ 116 | :return: the path and content of main.cpp in a test module 117 | """ 118 | return { 119 | "gtest": self.create_gtest_main, 120 | "catch2": self.create_nothing, 121 | "unknown": self.create_nothing 122 | }.get(self.test_framework, self.create_nothing)() 123 | 124 | def create_gtest_main(self): 125 | content = \ 126 | """#include 127 | 128 | int main(int argc, char **argv) { 129 | ::testing::InitGoogleTest(&argc, argv); 130 | return RUN_ALL_TESTS(); 131 | } 132 | """ 133 | return f'{self.path}/main.cpp', content 134 | 135 | def create_nothing(self): 136 | return None, None 137 | -------------------------------------------------------------------------------- /create_cmake_project.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import runpy 3 | 4 | if __name__ == "__main__": 5 | runpy.run_module("cmake_project_creator", run_name='__main__') 6 | -------------------------------------------------------------------------------- /examples/dual.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "Dual", 3 | "c++Standard": "17", 4 | "directories": [{ 5 | "name": "executable", 6 | "type": "intermediate", 7 | "subdirectories": [{ 8 | "name": "src", 9 | "type": "source", 10 | "library": null, 11 | "executable": "true", 12 | "include": "true", 13 | "dependencies": [{ 14 | "type": "internal", 15 | "link": "library/src" 16 | }], 17 | "subdirectories": [] 18 | }, 19 | { 20 | "name": "include", 21 | "type": "include", 22 | "subdirectories": [] 23 | }, 24 | { 25 | "name": "tests", 26 | "type": "tests", 27 | "dependencies": [{ 28 | "type": "conan", 29 | "name": "gtest", 30 | "version": "1.8.1" 31 | }], 32 | "subdirectories": [] 33 | } 34 | ] 35 | }, 36 | { 37 | "name": "library", 38 | "type": "intermediate", 39 | "subdirectories": [{ 40 | "name": "src", 41 | "type": "source", 42 | "library": "static", 43 | "executable": "true", 44 | "executable_name": "dual_lib_custom_executable", 45 | "library_name": "dualCustomLibraryName", 46 | "include": "true", 47 | "dependencies": [], 48 | "subdirectories": [] 49 | }, 50 | { 51 | "name": "include", 52 | "type": "include", 53 | "subdirectories": [] 54 | }, 55 | { 56 | "name": "tests", 57 | "type": "tests", 58 | "dependencies": [{ 59 | "type": "conan", 60 | "name": "gtest", 61 | "version": "1.8.1" 62 | }], 63 | "subdirectories": [] 64 | } 65 | ] 66 | } 67 | ] 68 | } -------------------------------------------------------------------------------- /examples/nested_dual.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "NestedDual", 3 | "c++Standard": "17", 4 | "directories": [{ 5 | "name": "common_root", 6 | "type": "intermediate", 7 | "subdirectories": [ 8 | 9 | { 10 | "name": "executable", 11 | "type": "intermediate", 12 | "subdirectories": [{ 13 | "name": "src", 14 | "type": "source", 15 | "library": null, 16 | "executable": "true", 17 | "include": "true", 18 | "dependencies": [{ 19 | "type": "internal", 20 | "link": "common_root/library/src" 21 | }], 22 | "subdirectories": [] 23 | }, 24 | { 25 | "name": "include", 26 | "type": "include", 27 | "subdirectories": [] 28 | }, 29 | { 30 | "name": "tests", 31 | "type": "tests", 32 | "dependencies": [{ 33 | "type": "conan", 34 | "name": "gtest", 35 | "version": "1.8.1" 36 | }], 37 | "subdirectories": [] 38 | } 39 | ] 40 | }, 41 | { 42 | "name": "library", 43 | "type": "intermediate", 44 | "subdirectories": [{ 45 | "name": "src", 46 | "type": "source", 47 | "library": "static", 48 | "executable": "true", 49 | "include": "true", 50 | "dependencies": [], 51 | "subdirectories": [] 52 | }, 53 | { 54 | "name": "include", 55 | "type": "include", 56 | "subdirectories": [] 57 | }, 58 | { 59 | "name": "tests", 60 | "type": "tests", 61 | "dependencies": [{ 62 | "type": "conan", 63 | "name": "gtest", 64 | "version": "1.8.1" 65 | }], 66 | "subdirectories": [] 67 | } 68 | ] 69 | } 70 | ] 71 | }] 72 | } -------------------------------------------------------------------------------- /examples/single.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "Tennis", 3 | "c++Standard": "20", 4 | "directories": [{ 5 | "name": "src", 6 | "type": "source", 7 | "library": "shared", 8 | "executable": "true", 9 | "include": "true", 10 | "dependencies": [], 11 | "subdirectories": [] 12 | }, 13 | { 14 | "name": "include", 15 | "type": "include", 16 | "subdirectories": [] 17 | }, 18 | { 19 | "name": "tests", 20 | "type": "tests", 21 | "dependencies": [{ 22 | "type": "conan2", 23 | "name": "gtest", 24 | "version": "1.14.0" 25 | }], 26 | "subdirectories": [] 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /examples/single_catch2.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "MyTestProjectSingle", 3 | "c++Standard": "17", 4 | "directories": [{ 5 | "name": "src", 6 | "type": "source", 7 | "library": "static", 8 | "executable": "true", 9 | "include": "true", 10 | "dependencies": [], 11 | "subdirectories": [] 12 | }, 13 | { 14 | "name": "include", 15 | "type": "include", 16 | "subdirectories": [] 17 | }, 18 | { 19 | "name": "tests", 20 | "type": "tests", 21 | "dependencies": [{ 22 | "type": "conan", 23 | "name": "catch2", 24 | "version": "3.1.0" 25 | }], 26 | "subdirectories": [] 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /examples/single_with_boost.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "MyTestProjectSingle", 3 | "c++Standard": "17", 4 | "directories": [{ 5 | "name": "src", 6 | "type": "source", 7 | "library": "shared", 8 | "executable": "true", 9 | "include": "true", 10 | "dependencies": [{ 11 | "type": "conan", 12 | "name": "boost", 13 | "version": "1.80.0" 14 | }], 15 | "subdirectories": [] 16 | }, 17 | { 18 | "name": "include", 19 | "type": "include", 20 | "subdirectories": [] 21 | }, 22 | { 23 | "name": "tests", 24 | "type": "tests", 25 | "dependencies": [{ 26 | "type": "conan", 27 | "name": "gtest", 28 | "version": "1.8.1" 29 | }], 30 | "subdirectories": [] 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /examples/single_with_compiler_options.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "MyTestProjectSingle", 3 | "c++Standard": "17", 4 | "compilerOptions": [ 5 | "-Wall", "-Wextra", "-Wpedantic", "-Werror" 6 | ], 7 | "directories": [{ 8 | "name": "src", 9 | "type": "source", 10 | "library": "shared", 11 | "executable": "true", 12 | "include": "true", 13 | "dependencies": [], 14 | "subdirectories": [] 15 | }, 16 | { 17 | "name": "include", 18 | "type": "include", 19 | "subdirectories": [] 20 | }, 21 | { 22 | "name": "tests", 23 | "type": "tests", 24 | "dependencies": [{ 25 | "type": "conan", 26 | "name": "gtest", 27 | "version": "1.8.1" 28 | }], 29 | "subdirectories": [] 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandordargo/cmake-project-creator/3c3ec396210be344e071a13cca89c9d07f6882d1/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_dependency.py: -------------------------------------------------------------------------------- 1 | from nose.tools import raises 2 | import nose.tools 3 | 4 | from cmake_project_creator import dependency 5 | 6 | 7 | def test_make_dependencies_ignore_internals(): 8 | raw_dependencies = [{ 9 | "type": "internal", 10 | "link": "proj/othercode/src" 11 | }] 12 | 13 | dependencies = dependency.Dependency.make_dependencies(raw_dependencies) 14 | nose.tools.eq_(len(dependencies), 0) 15 | 16 | 17 | @raises(ValueError) 18 | def test_make_dependencies_raises_error_if_type_is_missing(): 19 | raw_dependencies = [{ 20 | "link": "proj/othercode/src" 21 | }] 22 | 23 | dependency.Dependency.make_dependencies(raw_dependencies) 24 | 25 | 26 | @raises(ValueError) 27 | def test_make_dependencies_raises_error_if_name_is_missing_for_non_internal(): 28 | raw_dependencies = [{ 29 | "type": "conan", 30 | }] 31 | 32 | dependency.Dependency.make_dependencies(raw_dependencies) 33 | 34 | 35 | def test_make_dependencies_conan_is_made(): 36 | raw_dependencies = [{ 37 | "type": "conan", 38 | "name": "gtest", 39 | "version": "1.8.1" 40 | }] 41 | 42 | dependencies = dependency.Dependency.make_dependencies(raw_dependencies) 43 | nose.tools.eq_(len(dependencies), 1) 44 | nose.tools.eq_(dependency.Dependency("conan", "gtest", "1.8.1"), dependencies[0]) 45 | 46 | 47 | def test_dependency_does_not_equal_non_dependency_object(): 48 | a_dependency = dependency.Dependency("Foo", "Bar") 49 | 50 | class Person: 51 | """ 52 | dummy class so that we can test Dependency.__eq__ 53 | """ 54 | 55 | nose.tools.ok_(not a_dependency.__eq__(Person)) 56 | -------------------------------------------------------------------------------- /tests/test_directory.py: -------------------------------------------------------------------------------- 1 | from cmake_project_creator.directory import Directory 2 | import nose.tools 3 | 4 | 5 | def test_get_suffix(): 6 | directory = Directory("projects_root", "ProjectRoot/Suffix/Extension", 7 | {}, "DummyProjectFileName") 8 | nose.tools.eq_(directory.get_name_suffix(), "Suffix") 9 | 10 | 11 | def test_only_project_root(): 12 | directory = Directory("projects_root", "ProjectRoot", {}, "DummyProjectFileName") 13 | nose.tools.eq_(directory.get_name_suffix(), "ProjectRoot") 14 | 15 | 16 | def test_get_path_without_project_root(): 17 | directory = Directory("projects_root", "ProjectRoot/Suffix/Extension", 18 | {}, "DummyProjectFileName") 19 | nose.tools.eq_(directory.get_path_without_project_root(), "Suffix/Extension") 20 | 21 | 22 | def test_nice_formatting(): 23 | directory = Directory("projects_root", "ProjectRoot/Suffix/Extension", 24 | {}, "DummyProjectFileName") 25 | nose.tools.eq_(str(directory), "Directory(path: ProjectRoot/Suffix/Extension, description:{})") 26 | -------------------------------------------------------------------------------- /tests/test_directory_factory.py: -------------------------------------------------------------------------------- 1 | from nose.tools import raises 2 | import nose.tools 3 | 4 | from cmake_project_creator import test_directory, include_directory, \ 5 | directory_factory, source_directory 6 | 7 | 8 | def test_make_source(): 9 | description = { 10 | "name": "src", 11 | "type": "source", 12 | "library": None, 13 | "executable": "true", 14 | "include": "true", 15 | "dependencies": [{ 16 | "type": "internal", 17 | "link": "proj/othercode/src" 18 | }], 19 | "subdirectories": [] 20 | } 21 | directory = directory_factory.make("projects_root", "foo", description, "DummyProjectFileName") 22 | 23 | nose.tools.ok_(isinstance(directory, source_directory.SourceDirectory)) 24 | 25 | 26 | def test_make_include(): 27 | description = { 28 | "name": "include", 29 | "type": "include", 30 | "subdirectories": [] 31 | } 32 | directory = directory_factory.make("projects_root", "foo", description, "DummyProjectFileName") 33 | 34 | nose.tools.ok_(isinstance(directory, include_directory.IncludeDirectory)) 35 | 36 | 37 | def test_make_test(): 38 | description = { 39 | "name": "tests", 40 | "type": "tests", 41 | "dependencies": [{ 42 | "type": "conan", 43 | "name": "gtest", 44 | "version": "1.8.1" 45 | }], 46 | "subdirectories": [] 47 | } 48 | directory = directory_factory.make("projects_root", "foo", description, "DummyProjectFileName") 49 | 50 | nose.tools.ok_(isinstance(directory, test_directory.TestDirectory)) 51 | 52 | 53 | @raises(KeyError) 54 | def test_incorrect_type(): 55 | description = { 56 | "name": "tests", 57 | "type": "incorrect", 58 | "dependencies": [{ 59 | "type": "conan", 60 | "name": "gtest", 61 | "version": "1.8.1" 62 | }], 63 | "subdirectories": [] 64 | } 65 | directory_factory.make("projects_root", "foo", description, "DummyProjectFileName") 66 | -------------------------------------------------------------------------------- /tests/test_include_directory.py: -------------------------------------------------------------------------------- 1 | from cmake_project_creator import include_directory 2 | import nose.tools 3 | 4 | 5 | class TestIncludeDirectory: 6 | 7 | def test_header(self): 8 | directory = include_directory.IncludeDirectory("projects_root", 9 | "root/path", 10 | {}, 11 | "DummyProject", 12 | []) 13 | expected = \ 14 | """#pragma once 15 | 16 | class DummyProject { 17 | public: 18 | void hello(); 19 | }; 20 | """ 21 | actual_path, actual_content = directory.create_header_content() 22 | nose.tools.eq_('root/path/DummyProject.h', actual_path) 23 | nose.tools.eq_(expected, actual_content) 24 | -------------------------------------------------------------------------------- /tests/test_project_creator.py: -------------------------------------------------------------------------------- 1 | from nose.tools import raises 2 | import nose.tools 3 | 4 | from cmake_project_creator import dependency, project_creator, source_directory 5 | 6 | DIRECTORIES = [{ 7 | "name": "proj", 8 | "type": "intermediate", 9 | "subdirectories": [ 10 | 11 | { 12 | "name": "code", 13 | "type": "intermediate", 14 | "subdirectories": [{ 15 | "name": "src", 16 | "type": "source", 17 | "library": None, 18 | "executable": "true", 19 | "include": "true", 20 | "dependencies": [{ 21 | "type": "internal", 22 | "link": "proj/othercode/src" 23 | }], 24 | "subdirectories": [] 25 | }, 26 | { 27 | "name": "include", 28 | "type": "include", 29 | "subdirectories": [] 30 | }, 31 | { 32 | "name": "tests", 33 | "type": "tests", 34 | "dependencies": [{ 35 | "type": "conan", 36 | "name": "gtest", 37 | "version": "1.8.1" 38 | }], 39 | "subdirectories": [] 40 | } 41 | ] 42 | }, 43 | { 44 | "name": "othercode", 45 | "type": "intermediate", 46 | "subdirectories": [{ 47 | "name": "src", 48 | "type": "source", 49 | "library": "static", 50 | "executable": "true", 51 | "include": "true", 52 | "dependencies": [], 53 | "subdirectories": [] 54 | }, 55 | { 56 | "name": "include", 57 | "type": "include", 58 | "subdirectories": [] 59 | }, 60 | { 61 | "name": "tests", 62 | "type": "tests", 63 | "dependencies": [{ 64 | "type": "conan", 65 | "name": "gtest", 66 | "version": "1.8.1" 67 | }], 68 | "subdirectories": [] 69 | } 70 | ] 71 | } 72 | ] 73 | }] 74 | 75 | 76 | def test_collect_cmake_subdirectories(): 77 | paths = project_creator.collect_cmake_subdirectories(DIRECTORIES) 78 | nose.tools.eq_(['proj/code/src', 'proj/code/tests', 79 | 'proj/othercode/src', 'proj/othercode/tests'], paths) 80 | 81 | 82 | def test_parse_directories(): 83 | single_directory = [{ 84 | "name": "src", 85 | "type": "source", 86 | "executable": "true", 87 | "include": "true", 88 | "dependencies": [], 89 | "subdirectories": [] 90 | }] 91 | expected = {"src": source_directory.SourceDirectory("projects_root", "src", single_directory[0], 92 | "DummyProject", [])} 93 | parsed_directories = project_creator.parse_directories(single_directory, 94 | "projects_root", "", "DummyProject") 95 | nose.tools.eq_(expected, parsed_directories) 96 | 97 | 98 | def test_collect_conan_dependencies(): 99 | single_directory = [{ 100 | "name": "src", 101 | "type": "source", 102 | "executable": "true", 103 | "include": "true", 104 | "dependencies": [{ 105 | "type": "internal", 106 | "link": "proj/othercode/src" 107 | }], 108 | "subdirectories": [] 109 | }] 110 | 111 | conan_dependency = dependency.Dependency("conan", "gtest", "1.8.1") 112 | 113 | parsed_directories = [ 114 | source_directory.SourceDirectory("projects_root", "src", single_directory[0], 115 | "DummyProject", [conan_dependency])] 116 | nose.tools.eq_({'gtest': '1.8.1'}, project_creator.collect_conan_dependencies(parsed_directories)) 117 | 118 | def test_collect_conan2_dependencies(): 119 | single_directory = [{ 120 | "name": "src", 121 | "type": "source", 122 | "executable": "true", 123 | "include": "true", 124 | "dependencies": [{ 125 | "type": "internal", 126 | "link": "proj/othercode/src" 127 | }], 128 | "subdirectories": [] 129 | }] 130 | 131 | conan_dependency = dependency.Dependency("conan2", "gtest", "1.8.1") 132 | 133 | parsed_directories = [ 134 | source_directory.SourceDirectory("projects_root", "src", single_directory[0], 135 | "DummyProject", [conan_dependency])] 136 | nose.tools.eq_({'gtest': '1.8.1'}, project_creator.collect_conan2_dependencies(parsed_directories)) 137 | 138 | 139 | 140 | @raises(ValueError) 141 | def test_conflicting_conan_deps(): 142 | single_directory = [{ 143 | "name": "src", 144 | "type": "source", 145 | "executable": "true", 146 | "include": "true", 147 | "dependencies": [{ 148 | "type": "internal", 149 | "link": "proj/othercode/src" 150 | }], 151 | "subdirectories": [] 152 | }] 153 | 154 | conan_dependency = dependency.Dependency("conan", "gtest", "1.8.1") 155 | conan_dependency2 = dependency.Dependency("conan", "gtest", "1.7.1") 156 | 157 | parsed_directories = [ 158 | source_directory.SourceDirectory("projects_root", "src", single_directory[0], 159 | "DummyProject", [conan_dependency, conan_dependency2])] 160 | print(project_creator.collect_conan_dependencies(parsed_directories)) 161 | 162 | 163 | @raises(ValueError) 164 | def test_unsupported_cpp_standard(): 165 | _, _ = project_creator.create_main_cmakelists("projects_root", "dummy", 166 | ["src", "test"], "12") 167 | 168 | def test_create_main_cmakelists_default_version(): 169 | expected = \ 170 | """cmake_minimum_required(VERSION 3.10) 171 | project(dummy) 172 | set(CMAKE_CXX_STANDARD 17) 173 | 174 | add_subdirectory("src") 175 | add_subdirectory("test") 176 | """ 177 | actual_path, actual_content = project_creator.create_main_cmakelists("projects_root", "dummy", 178 | ["src", "test"], None) 179 | 180 | nose.tools.eq_(actual_content, expected) 181 | nose.tools.eq_(actual_path, "projects_root/dummy/CMakeLists.txt") 182 | 183 | 184 | def test_create_main_cmakelists_with_compiler_options(): 185 | expected = \ 186 | """cmake_minimum_required(VERSION 3.10) 187 | project(dummy) 188 | set(CMAKE_CXX_STANDARD 17) 189 | add_compile_options(-Wall -Wextra -Wpedantic -Werror) 190 | 191 | add_subdirectory("src") 192 | add_subdirectory("test") 193 | """ 194 | actual_path, actual_content = project_creator.create_main_cmakelists("projects_root", "dummy", 195 | ["src", "test"], None, ["-Wall", "-Wextra", "-Wpedantic", "-Werror"]) 196 | 197 | nose.tools.eq_(actual_content, expected, "%r != %r" % (actual_content, expected)) 198 | nose.tools.eq_(actual_path, "projects_root/dummy/CMakeLists.txt") 199 | 200 | 201 | 202 | def test_create_main_cmakelists(): 203 | expected = \ 204 | """cmake_minimum_required(VERSION 3.10) 205 | project(dummy) 206 | set(CMAKE_CXX_STANDARD 14) 207 | 208 | add_subdirectory("src") 209 | add_subdirectory("test") 210 | """ 211 | actual_path, actual_content = project_creator.create_main_cmakelists("projects_root", "dummy", 212 | ["src", "test"], "14") 213 | 214 | nose.tools.eq_(actual_content, expected) 215 | nose.tools.eq_(actual_path, "projects_root/dummy/CMakeLists.txt") 216 | 217 | def test_collect_compiler_options(): 218 | project_description = { "compilerOptions": [ 219 | "-Wall", "-Wextra", "-Wpedantic", "-Werror" 220 | ]} 221 | expected = ["-Wall", "-Wextra", "-Wpedantic", "-Werror"] 222 | 223 | nose.tools.eq_(project_creator.collect_compiler_options(project_description), expected) 224 | 225 | def test_collect_compiler_options_missing_input(): 226 | project_description = {} 227 | expected = [] 228 | 229 | nose.tools.eq_(project_creator.collect_compiler_options(project_description), expected) 230 | 231 | def test_create_runtest_with_conan2(): 232 | expected = """CURRENT_DIR=`pwd` 233 | rm -rf build && mkdir build && conan install . --output-folder=build --build=missing && cd build && cmake .. -DCMAKE_TOOLCHAIN_FILE=build/Release/generators/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release && cmake --build . && (./tests/dummy_tests_test) 234 | cd "${CURRENT_DIR}" 235 | """ 236 | subdirectories = ['src', 'tests'] 237 | 238 | nose.tools.eq_(project_creator.create_runtest(2, "dummy", ['src', 'tests']), expected) 239 | 240 | def test_create_runtest_with_conan1(): 241 | expected = """CURRENT_DIR=`pwd` 242 | rm -rf build && mkdir build && conan install . -s compiler.libcxx=libstdc++11 && cd build && cmake .. && cmake --build . && (./tests/dummy_tests_test) 243 | cd "${CURRENT_DIR}" 244 | """ 245 | subdirectories = ['src', 'tests'] 246 | 247 | nose.tools.eq_(project_creator.create_runtest(1, "dummy", ['src', 'tests']), expected) 248 | 249 | 250 | def test_create_conanfile(): 251 | expected = \ 252 | """[requires] 253 | gtest/1.8.1 254 | 255 | [generators] 256 | cmake 257 | """ 258 | conan_dependencies = {"gtest": "1.8.1"} 259 | nose.tools.eq_(project_creator.create_conanfile(conan_dependencies), expected) 260 | 261 | def test_create_conanfile2(): 262 | expected = \ 263 | """[requires] 264 | gtest/1.14.0 265 | 266 | [generators] 267 | CMakeDeps 268 | CMakeToolchain 269 | 270 | [layout] 271 | cmake_layout 272 | """ 273 | conan2_dependencies = {"gtest": "1.14.0"} 274 | nose.tools.eq_(project_creator.create_conanfile2(conan2_dependencies), expected) 275 | -------------------------------------------------------------------------------- /tests/test_source_directory.py: -------------------------------------------------------------------------------- 1 | from nose.tools import raises 2 | import nose.tools 3 | 4 | from cmake_project_creator import directory, source_directory 5 | 6 | 7 | @raises(ValueError) 8 | def test_raise_if_invalid_dependency(): 9 | description = { 10 | "name": "tests", 11 | "type": "tests", 12 | "dependencies": [{ 13 | "type": "internal", 14 | "link": "proj/othercode/src" 15 | }], 16 | "subdirectories": [] 17 | } 18 | source_directory.raise_if_invalid_dependency({ 19 | "type": "internal", 20 | "link": "proj/othercode/src" 21 | }, [directory.Directory("projects_root", "path", description, "DummyProject")]) 22 | 23 | 24 | @raises(KeyError) 25 | def test_raise_if_invalid_dependency_when_all_empty(): 26 | source_directory.raise_if_invalid_dependency({}, []) 27 | 28 | 29 | def test_get_include_library_of_directory(): 30 | path = "root/project/src" 31 | expected = "project/include" 32 | nose.tools.eq_(expected, source_directory.get_include_library_of_directory( 33 | directory.Directory("projects_root", path, {}, "", []))) 34 | 35 | 36 | def test_creates_main(): 37 | description = { 38 | "name": "src", 39 | "type": "source", 40 | "library": None, 41 | "executable": "true", 42 | "include": "true", 43 | "dependencies": [{ 44 | "type": "internal", 45 | "link": "proj/othercode/src" 46 | }], 47 | "subdirectories": [] 48 | } 49 | source_dir = source_directory.SourceDirectory("projects_root", 50 | "root/path", 51 | description, 52 | "DummyProject", 53 | []) 54 | expected = \ 55 | """#include "DummyProject.h" 56 | 57 | int main() { 58 | DummyProject o; 59 | o.hello(); 60 | } 61 | """ 62 | actual_path, actual_content = source_dir.create_main(None) 63 | nose.tools.eq_('root/path/main.cpp', actual_path) 64 | nose.tools.eq_(expected, actual_content) 65 | 66 | 67 | def test_empty_creates_main(): 68 | description = { 69 | "name": "src", 70 | "type": "source", 71 | "library": None, 72 | "executable": "false", 73 | "include": "true", 74 | "dependencies": [{ 75 | "type": "internal", 76 | "link": "proj/othercode/src" 77 | }], 78 | "subdirectories": [] 79 | } 80 | source_dir = source_directory.SourceDirectory("projects_root", 81 | "root/path", 82 | description, 83 | "DummyProject", 84 | []) 85 | expected = "" 86 | actual_path, actual_content = source_dir.create_main(None) 87 | nose.tools.eq_(None, actual_path) 88 | nose.tools.eq_(expected, actual_content) 89 | 90 | 91 | def test_source_file(): 92 | description = { 93 | "name": "src", 94 | "type": "source", 95 | "library": None, 96 | "executable": "true", 97 | "include": "true", 98 | "dependencies": [{ 99 | "type": "internal", 100 | "link": "proj/othercode/src" 101 | }], 102 | "subdirectories": [] 103 | } 104 | source_dir = source_directory.SourceDirectory("projects_root", 105 | "root/path", 106 | description, 107 | "DummyProject", 108 | []) 109 | expected = \ 110 | """#include "DummyProject.h" 111 | #include 112 | 113 | void DummyProject::hello() { 114 | std::cout << "hello" << std::endl; 115 | } 116 | """ 117 | actual_path, actual_content = source_dir.create_source_file(None) 118 | nose.tools.eq_('root/path/DummyProject.cpp', actual_path) 119 | nose.tools.eq_(expected, actual_content) 120 | 121 | 122 | def test_create_cmakelists_conan(): 123 | description = { 124 | "name": "src", 125 | "type": "source", 126 | "library": None, 127 | "executable": "true", 128 | "include": "true", 129 | "dependencies": [], 130 | "subdirectories": [] 131 | } 132 | source_dir = source_directory.SourceDirectory("projects_root", 133 | "root/path", 134 | description, 135 | "DummyProject", 136 | []) 137 | expected = \ 138 | """set(BINARY ${CMAKE_PROJECT_NAME}_path) 139 | 140 | 141 | 142 | 143 | 144 | 145 | include_directories(../include) 146 | 147 | 148 | file(GLOB SOURCES *.cpp) 149 | set(SOURCES ${SOURCES}) 150 | 151 | add_executable(DummyProject_path ${SOURCES}) 152 | 153 | """ 154 | actual_path, actual_content = source_dir.create_cmakelists([]) 155 | print(actual_content) 156 | nose.tools.eq_('root/path/CMakeLists.txt', actual_path) 157 | nose.tools.eq_(expected, actual_content, "%r != %r" % (expected, actual_content)) 158 | 159 | 160 | @raises(ValueError) 161 | def test_create_cmakelists_conan_error(): 162 | description = { 163 | "name": "src", 164 | "type": "source", 165 | "library": None, 166 | "executable": "true", 167 | "include": "true", 168 | "dependencies": [{ 169 | "type": "internal", 170 | "link": "proj/othercode/src" 171 | }], 172 | "subdirectories": [] 173 | } 174 | source_dir = source_directory.SourceDirectory("projects_root", 175 | "root/path", 176 | description, 177 | "DummyProject", 178 | []) 179 | source_dir.create_cmakelists([]) 180 | 181 | 182 | def test_custom_library_name(): 183 | description = { 184 | "name": "src", 185 | "type": "source", 186 | "library": "static", 187 | "executable": "true", 188 | "executable_name": "dual_lib_custom_executable", 189 | "library_name": "dualCustomLibraryName", 190 | "include": "true", 191 | "dependencies": [], 192 | "subdirectories": [] 193 | } 194 | 195 | source_dir = source_directory.SourceDirectory("projects_root", 196 | "root/path", 197 | description, 198 | "DummyProject", 199 | []) 200 | nose.tools.eq_(source_dir.build_library_command().startswith("add_library(dualCustomLibraryName STATIC"), True) 201 | 202 | 203 | def test_custom_executable_name(): 204 | description = { 205 | "name": "src", 206 | "type": "source", 207 | "library": "static", 208 | "executable": "true", 209 | "executable_name": "dual_lib_custom_executable", 210 | "library_name": "dualCustomLibraryName", 211 | "include": "true", 212 | "dependencies": [], 213 | "subdirectories": [] 214 | } 215 | 216 | source_dir = source_directory.SourceDirectory("projects_root", 217 | "root/path", 218 | description, 219 | "DummyProject", 220 | []) 221 | nose.tools.eq_(source_dir.build_executable_command().startswith("add_executable(dual_lib_custom_executable"), True) 222 | 223 | 224 | def test_string_representation(): 225 | description = { 226 | "name": "src", 227 | "type": "source", 228 | "library": None, 229 | "executable": "true", 230 | "include": "true", 231 | "dependencies": [{ 232 | "type": "internal", 233 | "link": "proj/othercode/src" 234 | }], 235 | "subdirectories": [] 236 | } 237 | source_dir = source_directory.SourceDirectory("projects_root", 238 | "root/path", 239 | description, 240 | "DummyProject", 241 | []) 242 | 243 | nose.tools.eq_(str(source_dir), "Directory(path: root/path, " \ 244 | "description:{'name': 'src', 'type': 'source', 'library': None, " \ 245 | "'executable': 'true', 'include': 'true', 'dependencies': [" \ 246 | "{'type': 'internal', 'link': 'proj/othercode/src'}], " \ 247 | "'subdirectories': []})") 248 | -------------------------------------------------------------------------------- /tests/test_test_directory.py: -------------------------------------------------------------------------------- 1 | from cmake_project_creator import test_directory, dependency 2 | 3 | import nose.tools 4 | 5 | def test_create_main(): 6 | test_dir = test_directory.TestDirectory("projects_root", "root/path", {}, "DummyProject", 7 | [dependency.Dependency("conan", "gtest", "1.8.1")]) 8 | expected = \ 9 | """#include 10 | 11 | int main(int argc, char **argv) { 12 | ::testing::InitGoogleTest(&argc, argv); 13 | return RUN_ALL_TESTS(); 14 | } 15 | """ 16 | actual_path, actual_content = test_dir.create_main({}) 17 | nose.tools.eq_(actual_path, 'root/path/main.cpp') 18 | nose.tools.eq_(actual_content, expected) 19 | 20 | 21 | def test_source_file(): 22 | test_dir = test_directory.TestDirectory("projects_root", "root/path", {}, "DummyProject", 23 | [dependency.Dependency("conan", "gtest", "1.8.1")]) 24 | expected = \ 25 | """#include 26 | #include "DummyProject.h" 27 | 28 | TEST(blaTest, test1) { 29 | DummyProject x; 30 | x.hello(); 31 | ASSERT_EQ (1, 0); 32 | } 33 | """ 34 | actual_path, actual_content = test_dir.create_source_file({}) 35 | 36 | nose.tools.eq_(actual_path, 'root/path/TestDummyProject.cpp') 37 | nose.tools.eq_(actual_content, expected) 38 | 39 | 40 | def test_create_cmakelists_no_conan(): 41 | test_dir = test_directory.TestDirectory("projects_root", "root/path", {}, "DummyProject", []) 42 | expected = \ 43 | """set(BINARY ${CMAKE_PROJECT_NAME}_path_test) 44 | file(GLOB_RECURSE TEST_SOURCES LIST_DIRECTORIES false *.h *.cpp) 45 | 46 | set(SOURCES ${TEST_SOURCES}) 47 | 48 | 49 | 50 | 51 | include_directories(../include) 52 | 53 | add_executable(${BINARY} ${TEST_SOURCES}) 54 | target_link_libraries(${BINARY} PUBLIC) 55 | 56 | add_test(NAME ${BINARY} COMMAND ${BINARY}) 57 | """ 58 | actual_path, actual_content = test_dir.create_cmakelists({}) 59 | nose.tools.eq_(actual_path, 'root/path/CMakeLists.txt') 60 | nose.tools.eq_(actual_content, expected) 61 | 62 | 63 | def test_create_cmakelists_conan(): 64 | description = { 65 | "name": "tests", 66 | "type": "tests", 67 | "dependencies": [{ 68 | "type": "conan", 69 | "name": "gtest", 70 | "version": "1.8.1" 71 | }], 72 | "subdirectories": [] 73 | } 74 | conan_dependency = dependency.Dependency("conan", "gtest", "1.8.1") 75 | test_dir = test_directory.TestDirectory("projects_root", "root/path", description, 76 | "DummyProject", [conan_dependency]) 77 | expected = \ 78 | """set(BINARY ${CMAKE_PROJECT_NAME}_path_test) 79 | file(GLOB_RECURSE TEST_SOURCES LIST_DIRECTORIES false *.h *.cpp) 80 | 81 | set(SOURCES ${TEST_SOURCES}) 82 | 83 | include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake) 84 | conan_basic_setup() # Prepares the CMakeList.txt for Conan. 85 | 86 | include_directories(../include) 87 | 88 | add_executable(${BINARY} ${TEST_SOURCES}) 89 | target_link_libraries(${BINARY} PUBLIC ${CONAN_LIBS}) 90 | 91 | add_test(NAME ${BINARY} COMMAND ${BINARY}) 92 | """ 93 | actual_path, actual_content = test_dir.create_cmakelists({}) 94 | print(actual_content) 95 | nose.tools.eq_(actual_path, 'root/path/CMakeLists.txt') 96 | nose.tools.eq_(actual_content, expected) 97 | 98 | def test_create_cmakelists_conan2(): 99 | description = { 100 | "name": "tests", 101 | "type": "tests", 102 | "dependencies": [{ 103 | "type": "conan2", 104 | "name": "gtest", 105 | "version": "1.8.1" 106 | }], 107 | "subdirectories": [] 108 | } 109 | conan_dependency = dependency.Dependency("conan2", "gtest", "1.8.1") 110 | test_dir = test_directory.TestDirectory("projects_root", "root/path", description, 111 | "DummyProject", [conan_dependency]) 112 | expected = \ 113 | """set(BINARY ${CMAKE_PROJECT_NAME}_path_test) 114 | file(GLOB_RECURSE TEST_SOURCES LIST_DIRECTORIES false *.h *.cpp) 115 | 116 | set(SOURCES ${TEST_SOURCES}) 117 | find_package(GTest REQUIRED) 118 | 119 | 120 | 121 | include_directories(../include) 122 | 123 | add_executable(${BINARY} ${TEST_SOURCES}) 124 | target_link_libraries(${BINARY} PUBLIC gtest::gtest) 125 | 126 | add_test(NAME ${BINARY} COMMAND ${BINARY}) 127 | """ 128 | actual_path, actual_content = test_dir.create_cmakelists({}) 129 | print(actual_content) 130 | nose.tools.eq_(actual_path, 'root/path/CMakeLists.txt') 131 | nose.tools.eq_(actual_content, expected) 132 | --------------------------------------------------------------------------------