Git Product home page Git Product logo

gersemi's Introduction

gersemi

Status License: MPL 2.0 Code style: black

A formatter to make your CMake code the real treasure.

Installation

You can install gersemi from PyPI:

pip3 install gersemi

Usage

usage: gersemi [-c] [-i] [--diff] [--default-config] [--version] [-h] [-l INTEGER]
               [--indent (INTEGER | tabs)] [--unsafe] [-q] [--color]
               [--definitions src [src ...]]
               [--list-expansion {favour-inlining,favour-expansion}] [-w INTEGER]
               [src ...]

A formatter to make your CMake code the real treasure.

positional arguments:
  src                   File or directory to format. If only `-` is provided, input is
                        taken from stdin instead.

modes:
  -c, --check           Check if files require reformatting. Return 0 when there's
                        nothing to reformat. Return 1 when some files would be
                        reformatted.
  -i, --in-place        Format files in-place.
  --diff                Show diff on stdout for each formatted file instead.
  --default-config      Generate default .gersemirc configuration file.
  --version             Show version.
  -h, --help            Show this help message and exit.

configuration:
  By default configuration is loaded from YAML formatted .gersemirc file if it's
  available. This file should be placed in one of the common parent directories of
  source files. Arguments from command line can be used to override parts of that
  configuration or supply them in absence of configuration file.

  -l INTEGER, --line-length INTEGER
                        Maximum line length in characters. [default: 80]
  --indent (INTEGER | tabs)
                        Number of spaces used to indent or 'tabs' for indenting with tabs
                        [default: 4]
  --unsafe              Skip default sanity checks.
  -q, --quiet           Skip printing non-error messages to stderr.
  --color               If --diff is selected showed diff is colorized.
  --definitions src [src ...]
                        Files or directories containing custom command definitions
                        (functions or macros). If only - is provided custom definitions,
                        if there are any, are taken from stdin instead. Commands from not
                        deprecated CMake native modules don't have to be provided. See:
                        https://cmake.org/cmake/help/latest/manual/cmake-modules.7.html
  --list-expansion {favour-inlining,favour-expansion}
                        Switch controls how code is expanded into multiple lines when
                        it's not possible to keep it formatted in one line. With "favour-
                        inlining" the list of entities will be formatted in such way that
                        sublists might still be formatted into single line as long as
                        it's possible or as long as it doesn't break the "more than four
                        standalone arguments" heuristic that's mostly focused on commands
                        like `set` or `list(APPEND)`. With "favour-expansion" the list of
                        entities will be formatted in such way that sublists will be
                        completely expanded once expansion becomes necessary at all.
                        [default: favour-inlining]
  -w INTEGER, --workers INTEGER
                        Number of workers used to format multiple files in parallel.
                        [default: number of CPUs on this system]

You can use gersemi with a pre-commit hook by adding the following to .pre-commit-config.yaml of your repository:

repos:
- repo: https://github.com/BlankSpruce/gersemi
  rev: 0.14.0
  hooks:
  - id: gersemi

Update rev to relevant version used in your repository. For more details refer to https://pre-commit.com/#using-the-latest-version-for-a-repository

Formatting

The key goal is for the tool to "just work" and to have as little configuration as possible so that you don't have to worry about fine-tuning formatter to your needs - as long as you embrace the gersemi style of formatting, similarly as black or gofmt do their job. The basic assumption is that code to format is valid CMake language code - gersemi might be able to format some particular cases of invalid code but it's not guaranteed and it shouldn't be relied upon. Moreover only commands from CMake 3.0 onwards are supported and will be formatted properly - for instance exec_program has been deprecated since CMake 3.0 so it won't be formatted. Changes to code might be destructive and you should always have a backup (version control helps a lot).

Style

gersemi in general will use canonical casing as it's defined in official CMake documentation like FetchContent_Declare. There are a few deliberate exceptions for which lower case name was chosen to provide broader consistency with other CMake commands. In case of unknown commands, not provided through definitions, lower case will be used.

Default style favour-inlining

gersemi will try to format the code in a way that respects set character limit for single line and only break line whenever necessary with one exception. The commands that have a group of parameters that aren't attached to any specific keyword (like set or list(APPEND)) will be broken into multiple lines when there are more than 4 arguments in that group. The exception to the rule is made as a heuristic to avoid large local diff when the given command won't fit into maximum line length.

Example:

# Four elements in the list "Oceans_Eleven"
set(Oceans_Eleven Danny Frank Rusty Reuben)

# Five elements in the list "Oceans_Twelve"
set(Oceans_Twelve
    Danny
    Frank
    Rusty
    Reuben
    Tess
)

favour-inlining style example:

cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
project(example CXX)

message(STATUS "This is example project")
message(
    STATUS
    "Here is yet another but much much longer message that should be displayed"
)

# project version
set(VERSION_MAJOR 0)
set(VERSION_MINOR 1)
set(VERSION_PATCH 0)

add_compile_options(
    -Wall
    -Wpedantic
    -fsanitize=address
    -fconcepts
    -fsomething-else
)

if(NOT ${SOME_OPTION})
    add_compile_options(-Werror)
endif()

# foobar library
add_library(foobar)
add_library(example::foobar ALIAS foobar)

target_sources(
    foobar
    PUBLIC
        include/some_subdirectory/header.hpp
        include/another_subdirectory/header.hpp
    PRIVATE
        src/some_subdirectory/src1.cpp
        src/some_subdirectory/src1.cpp
        src/another_subdirectory/src1.cpp
        src/another_subdirectory/src2.cpp
        src/another_subdirectory/src3.cpp
)

target_include_directories(
    foobar
    INTERFACE
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
        $<INSTALL_INTERFACE:include>
)

target_link_libraries(
    foobar
    PUBLIC example::dependency_one example::dependency_two
    PRIVATE
        example::some_util
        external::some_lib
        external::another_lib
        Boost::Boost
)

include(GNUInstallDirs)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR})

# example executable
add_executable(app main.cpp)
target_link_libraries(app PRIVATE example::foobar Boost::Boost)

# tests
include(CTest)
include(GTest)
enable_testing()
add_subdirectory(tests)

# some helper function - see more details in "Let's make a deal" section
function(add_test_executable)
    set(OPTIONS
        QUIET
        VERBOSE
        SOME_PARTICULARLY_LONG_KEYWORD_THAT_ENABLES_SOMETHING
    )
    set(ONE_VALUE_ARGS NAME TESTED_TARGET)
    set(MULTI_VALUE_ARGS SOURCES DEPENDENCIES)

    cmake_parse_arguments(
        THIS_FUNCTION_PREFIX
        ${OPTIONS}
        ${ONE_VALUE_ARGS}
        ${MULTI_VALUE_ARGS}
    )
    # rest of the function
endfunction()

add_test_executable(
    NAME foobar_tests
    TESTED_TARGET foobar
    SOURCES
        some_test1.cpp
        some_test2.cpp
        some_test3.cpp
        some_test4.cpp
        some_test5.cpp
    QUIET
    DEPENDENCIES googletest::googletest
)

add_custom_command(
    OUTPUT ${SOMETHING_TO_OUTPUT}
    COMMAND ${CMAKE_COMMAND} -E cat foobar
    COMMAND cmake -E echo foobar
    COMMAND
        cmake -E echo "something quite a bit                           longer"
    WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/something
    DEPENDS
        ${CMAKE_CURRENT_SOURCE_DIR}/something
        ${CMAKE_CURRENT_SOURCE_DIR}/something_else
    COMMENT "example custom command"
)

Alternative style favour-expansion

In this style lines are broken in one of these cases:

  • there is at least one multi-value argument present a single command invocation, either keyworded one like PUBLIC in target_link_libraries or standalone one like list of files in add_library, which has more than one value
  • there are more than one multi-value arguments present in the command invocation like target_link_libraries with PUBLIC and PRIVATE arguments.
  • character limit for single line is reached

One-value arguments (like NAME in add_test) will be inlined unless that'd violate character limit. Structure or control flow commands (if, while, function, foreach etc.) are exempted from these special rules and follow the same formatting as favour-inlining. This style is more merge or git blame friendly because usually multi-value arguments are changed one element at a time and with this style such change will be visible as one line of code per element.

favour-expansion style example:

cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
project(example CXX)

message(STATUS "This is example project")
message(
    STATUS
    "Here is yet another but much much longer message that should be displayed"
)

# project version
set(VERSION_MAJOR 0)
set(VERSION_MINOR 1)
set(VERSION_PATCH 0)

add_compile_options(
    -Wall
    -Wpedantic
    -fsanitize=address
    -fconcepts
    -fsomething-else
)

if(NOT ${SOME_OPTION})
    add_compile_options(-Werror)
endif()

# foobar library
add_library(foobar)
add_library(example::foobar ALIAS foobar)

target_sources(
    foobar
    PUBLIC
        include/some_subdirectory/header.hpp
        include/another_subdirectory/header.hpp
    PRIVATE
        src/some_subdirectory/src1.cpp
        src/some_subdirectory/src1.cpp
        src/another_subdirectory/src1.cpp
        src/another_subdirectory/src2.cpp
        src/another_subdirectory/src3.cpp
)

target_include_directories(
    foobar
    INTERFACE
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
        $<INSTALL_INTERFACE:include>
)

target_link_libraries(
    foobar
    PUBLIC
        example::dependency_one
        example::dependency_two
    PRIVATE
        example::some_util
        external::some_lib
        external::another_lib
        Boost::Boost
)

include(GNUInstallDirs)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR})

# example executable
add_executable(app main.cpp)
target_link_libraries(
    app
    PRIVATE
        example::foobar
        Boost::Boost
)

# tests
include(CTest)
include(GTest)
enable_testing()
add_subdirectory(tests)

# some helper function - see more details in "Let's make a deal" section
function(add_test_executable)
    set(OPTIONS
        QUIET
        VERBOSE
        SOME_PARTICULARLY_LONG_KEYWORD_THAT_ENABLES_SOMETHING
    )
    set(ONE_VALUE_ARGS
        NAME
        TESTED_TARGET
    )
    set(MULTI_VALUE_ARGS
        SOURCES
        DEPENDENCIES
    )

    cmake_parse_arguments(
        THIS_FUNCTION_PREFIX
        ${OPTIONS}
        ${ONE_VALUE_ARGS}
        ${MULTI_VALUE_ARGS}
    )
    # rest of the function
endfunction()

add_test_executable(
    NAME foobar_tests
    TESTED_TARGET foobar
    SOURCES
        some_test1.cpp
        some_test2.cpp
        some_test3.cpp
        some_test4.cpp
        some_test5.cpp
    QUIET
    DEPENDENCIES googletest::googletest
)

add_custom_command(
    OUTPUT
        ${SOMETHING_TO_OUTPUT}
    COMMAND
        ${CMAKE_COMMAND} -E cat foobar
    COMMAND
        cmake -E echo foobar
    COMMAND
        cmake -E echo "something quite a bit                           longer"
    WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/something
    DEPENDS
        ${CMAKE_CURRENT_SOURCE_DIR}/something
        ${CMAKE_CURRENT_SOURCE_DIR}/something_else
    COMMENT "example custom command"
)

Let's make a deal

It's possible to provide reasonable formatting for custom commands. However on language level there are no hints available about supported keywords for given command so gersemi has to generate specialized formatter. To do that custom command definition is necessary which should be provided with --definitions. There are limitations though since it'd probably require full-blown CMake language interpreter to do it in every case so let's make a deal: if your custom command definition (function or macro) uses cmake_parse_arguments and does it in obvious manner such specialized formatter will be generated. Name casing used in command definition will be considered canonical for custom command (in the example below canonical casing will be Seven_Samurai). For instance this definition is okay (you can find other examples in tests/custom_command_formatting/):

function(Seven_Samurai some standalone arguments)
    set(options KAMBEI KATSUSHIRO)
    set(oneValueArgs GOROBEI HEIHACHI KYUZO)
    set(multiValueArgs SHICHIROJI KIKUCHIYO)

    cmake_parse_arguments(
        THIS_FUNCTION_PREFIX
        "${options}"
        "${oneValueArgs}"
        "${multiValueArgs}"
        ${ARGN}
    )

    # rest of the function definition...
endfunction()

With this definition available it's possible to format code like so:

Seven_Samurai(
    three
    standalone
    arguments
    KAMBEI
    KATSUSHIRO
    GOROBEI foo
    HEIHACHI bar
    KYUZO baz
    SHICHIROJI foo bar baz
    KIKUCHIYO bar baz foo
)

Otherwise gersemi will fallback to only fixing indentation of command name and it's closing parenthesis while preserving original formatting of arguments:

# before formatting of unknown command
  watch_david_fincher_movies(
       "Se7en"
       "The Game"
         "Fight Club"
       "Zodiac"     "The Curious Case of Benjamin Button"
         )

# after
watch_david_fincher_movies(
       "Se7en"
       "The Game"
         "Fight Club"
       "Zodiac"     "The Curious Case of Benjamin Button"
)

If you find these limitations too strict let me know about your case.

How to format custom commands for which path to definition can't be guaranteed to be stable? (e.g external dependencies not managed by CMake)

You can provide stub definitions that will be used only as an input for gersemi. Example:

# ./.gersemirc
definitions: [./src/cmake/stubs, ...] # ... other paths that might contain actual definitions
line_length: 120
list_expansion: favour-expansion
# ./src/cmake/stubs/try_to_win_best_picture_academy_award.cmake
# A stub for some external command out of our control
function(try_to_win_best_picture_academy_award)
    # gersemi: hints { CAST: pairs, SUMMARY: command_line }
    set(options FOREIGN_LANGUAGE)
    set(oneValueArgs GENRE YEAR)
    set(multiValueArgs DIRECTORS CAST SUMMARY)

    cmake_parse_arguments(_ "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
endfunction()

gersemi: ignore

If your definition should be ignored for purposes of generating specialized formatter you can use # gersemi: ignore at the beginning of the custom command:

function(harry_potter_and_the_philosophers_stone some standalone arguments)
    # gersemi: ignore
    set(options HARRY)
    set(oneValueArgs HERMIONE)
    set(multiValueArgs RON)

    cmake_parse_arguments(
        THIS_FUNCTION_PREFIX
        "${options}"
        "${oneValueArgs}"
        "${multiValueArgs}"
        ${ARGN}
    )

    # rest of the definition...
endfunction()

# no reformatting
harry_potter_and_the_philosophers_stone(HARRY
    HERMIONE foo
              RON foo bar baz)

It should be still preferred simply to not provide that definition instead.

gersemi: hints

If your definition has # gersemi: hints at the beginning then after hints you can provide YAML formatted pairs <keyword>: <specialized_formatting> to indicate how to treat specific multi-value arguments. <specialized_formatting> can be:

  • pairs: arguments after the keyword will be grouped into pairs, similar to how set_target_properties(PROPERTIES) is handled
  • command_line: arguments after the keyword will be treated like a sequence of words in command line, similar to how add_custom_command(COMMAND) is handled

Example:

function(movie_description_without_hints)
set(options "")
set(oneValueArgs DIRECTOR)
set(multiValueArgs CAST SUMMARY)

cmake_parse_arguments(THIS_FUNCTION_PREFIX "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
endfunction()

function(movie_description_with_hints)
# gersemi: hints { CAST: pairs, SUMMARY: command_line }
set(options "")
set(oneValueArgs DIRECTOR)
set(multiValueArgs CAST SUMMARY)

cmake_parse_arguments(THIS_FUNCTION_PREFIX "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
endfunction()

movie_description_without_hints(
    Oppenheimer
    DIRECTOR "Christopher Nolan"
    CAST
        "J. Robert Oppenheimer"
        "Cillian Murphy"
        "Kitty Oppenheimer"
        "Emily Blunt"
        "General Leslie Groves"
        "Matt Damon"
    SUMMARY
        Oppenheimer
        is
        an
        epic
        biographical
        thriller
        directed
        by
        Christopher
        Nolan.
)

movie_description_with_hints(
    Oppenheimer
    DIRECTOR "Christopher Nolan"
    CAST
        "J. Robert Oppenheimer" "Cillian Murphy"
        "Kitty Oppenheimer" "Emily Blunt"
        "General Leslie Groves" "Matt Damon"
    SUMMARY
        Oppenheimer is an epic biographical thriller directed by Christopher
        Nolan.
)

How to disable reformatting

Gersemi can be disallowed to format block of code using pair of comments # gersemi: off/# gersemi: on. Example:

the_hobbit(
    BURGLAR "Bilbo Baggins"
    WIZARD Gandalf
    DWARVES
        "Thorin Oakenshield"
        Fili
        Kili
        Balin
        Dwalin
        Oin
        Gloin
        Dori
        Nori
        Ori
        Bifur
        Bofur
        Bombur
)

# gersemi: off
the_fellowship_of_the_ring     (
    RING_BEARER Frodo GARDENER Samwise
    Merry Pippin Aragon
            Boromir
            Gimli
       Legolas
       Gandalf
       )
# gersemi: on

Pair of comments should be in the same scope, so the following is not supported:

# gersemi: off
the_godfather()

function(how_to_make_a_successful_movie args)
step_one_have_a_good_scenario()
# gersemi: on
step_two_make_the_movie()
endfunction()

Contributing

Bug or style inconsitencies reports are always welcomed. In case of style enhancement or feature proposals consider providing rationale (and maybe some example) having in mind the deliberate choice mentioned above. As long as it's meant to improve something go for it and be prepared to defend your point.

Running tests

Entire test suite can be run with just:

tox

Selecting functional tests can be done like so:

tox -e tests -- -k <test_pattern>

If you are familiar with pytest then you can pass relevant arguments after --.

gersemi's People

Contributors

blankspruce avatar danielchabrowski avatar fruzitent avatar thadhouse avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

gersemi's Issues

TypeError argument str not PosixPath

I installed a fresh library through pip3 install --user gersemi.
After installing I tried running gersemi . in the root of project directory.
But it game me the following error:

Traceback (most recent call last):
  File "/home/akshitsharma/.local/bin/gersemi", line 8, in <module>
    sys.exit(main())
  File "/home/akshitsharma/.local/lib/python3.6/site-packages/gersemi/__main__.py", line 163, in main
    sys.exit(run(mode, configuration, args.sources))
  File "/home/akshitsharma/.local/lib/python3.6/site-packages/gersemi/runner.py", line 151, in run
    with create_cache() as cache, create_pool(Path("-") in requested_files) as pool:
  File "/usr/lib/python3.6/contextlib.py", line 81, in __enter__
    return next(self.gen)
  File "/home/akshitsharma/.local/lib/python3.6/site-packages/gersemi/cache.py", line 97, in create_cache
    with database_cursor(cache_path()) as cursor:
  File "/usr/lib/python3.6/contextlib.py", line 81, in __enter__
    return next(self.gen)
  File "/home/akshitsharma/.local/lib/python3.6/site-packages/gersemi/cache.py", line 38, in database_cursor
    connection = sqlite3.connect(path, detect_types=sqlite3.PARSE_DECLTYPES)
TypeError: argument 1 must be str, not PosixPath

I am not sure what I am doing wrong or how to run it.
I also tried running gersemi $PWD but the same error pops up.

Here's the output from gersemi --version

gersemi 0.6.0
lark 0.9.0
Python 3.6.9 (default, Apr 18 2020, 01:56:04) 
[GCC 8.4.0]

Indentation for strings fails

Thank you for this tool! Finally no manual CMakeLists.txt formatting anymore :)

I have a scenario where the formatter seems to fail:

    rosidl_generate_interfaces(${PROJECT_NAME} 
      "msg/Duration.msg"
        "msg/DynamicTimePoint.msg"
      "msg/TimeSourceType.msg"
      "msg/SteadyTimePoint.msg"      "msg/SystemTimePoint.msg"
    )

I would have expected that it a) fixes the indentation and b) puts every item on a new line. I've read the Let's make a deal section but I am under the impression that this should work nonetheless.

Style change proposal: target and file lists should never be put on one line

First of all, I was really happy when I found gersemi, it looks pretty great, and it is maintained, unlike cmake-format, yay! I personally prefer less opinionated formatters, but the default style of gersemi fits our use case fairly well. However, there is one specific case where I feel like it could be improved, so I'm proposing a style enhancement.

The README shows this example of formatting:

target_link_libraries(
    foobar
    PUBLIC example::dependency_one example::dependency_two
    PRIVATE
        example::some_util
        external::some_lib
        external::another_lib
        Boost::Boost
)

I'd argue that target_link_libraries() and similar functions should always be formatted this way:

target_link_libraries(foobar
    PUBLIC
        example::dependency_one
        example::dependency_two
    PRIVATE
        example::some_util
        external::some_lib
        external::another_lib
        Boost::Boost
)

add_library() should also always be formatted this way:

add_library(lib SHARED
    source_one.cpp
    source_two.cpp
)

The two key changes:

  1. Even though the PUBLIC section would've fit on one line, every dependency was broken into a separate line.
  2. The target name and any valueless flags were moved to the same line as the function call.

Rationale for nr. 1:

  • At least in our codebase, file lists change quite often as people add, remove and move around code. This often makes dependency lists change too. Breaking these lists into separate lines make these diffs cleaner, which leads to less merge conflicts and easier to review pull requests, as git understands adding/removing lines better than changins a list in one line.
  • Building on that, if a particular target/file list oscillates around the limit for what can fit in one line, that can further obfuscate the change, as adding one file/target may trigger gersemi to break stuff into multiple lines, then removing something else may trigger it to put everything in one line again, causing unnecessarily large diffs.
  • Especially in a case like the first example where there's both a PUBLIC and PRIVATE section, it's more pleasing aesthetically for them to be formatted consistently.

Rationale for nr. 2:

  • I feel like these functions are tied stronger to the (first) target argument than the others, similarly how method calls treat the object argument special in OOP languages. (I.e. it's object.func(arg1, arg2) instead of func(object, arg1, arg2).) I'd consider the target_whatever() functions to be "methods" on the target object, and express this by putting them on the same line.
  • In case of add_library() / add_executable() without any flags, there is no clear separation between the target name and the list of files; putting the target name next to the function call and the files in separate lines with different indenting makes it more distinct.
  • Similarly, if there are any flags (STATIC/SHARED, EXCLUDE_FROM_ALL, etc.), those would look out of place "mixed in" with the file list.

I'd apply the above changes to add_library(), add_executable(), target_link_libraries(), target_sources(), target_precompile_headers(), and possibly target_include_directories(), target_link_directories(), target_compile_definitions(), target_compile_features(), target_compile_options(), target_link_options().

What do you think? Nr. 1 is the really important part for me for the 5 highlighted functions, the other functions and nr. 2 is more of a "if we're already talking about this" thing. I'm also torn on whether function calls with only one provided list argument containing one value (eg. target_include_directories(Foo PUBLIC include)) should warrant an exception to the "always multiline" rule.

Nondeterministic formatting of lists containing keywords

With Gersemi 0.13.5 (specifically commit 141df8c), when configured to favour-expansion, lists containing common keywords are sometimes formatted vertically as before, but sometimes horizontally.

Can be reproduced (with a few tries) by running gersemi --diff --color . in a directory containing these 3 files:

.gersemirc

list_expansion: favour-expansion

unrelated.cmake

target_link_libraries(unrelated PRIVATE pthread)

list_containing_keyword.cmake

set(list_containing_keyword
    PUBLIC
    pthread
)

Add support for ignoring files

Thank you for gersemi, it is a really helpful and time-saving tool.

I use gersemi in a medium-sized project, usually through the provided pre-commit hook. I noticed that CMake would always reconfigure my project afer running pre-commit run --all-files, even though pre-commit reported that no files had been modified.

After doing a little bit of investigation, I found out that the reconfiguration is triggered because gersemi formats some auto-generated CMake files from the build directory. This is the minimal CMake project I used to explore this issue.

cmake_minimum_required(VERSION 3.0)

project(test-project)

add_executable(${CMAKE_PROJECT_NAME} main.c)
#include <stdlib.h>

int main(void)
{
    return EXIT_SUCCESS;
}

This is the output of a gersemi dry-run after the first configuration. Only auto-generated files would be formatted, so running gersemi should not result in a reconfiguration.

$ gersemi -c .
~/test-project/build/cmake_install.cmake would be reformatted
~/test-project/build/CMakeFiles/Makefile.cmake would be reformatted
~/test-project/build/CMakeFiles/test-project.dir/DependInfo.cmake would be reformatted
~/test-project/build/CMakeFiles/3.22.1/CMakeSystem.cmake would be reformatted
~/test-project/build/CMakeFiles/test-project.dir/cmake_clean.cmake would be reformatted
~/test-project/build/CMakeFiles/3.22.1/CMakeCCompiler.cmake would be reformatted
~/test-project/build/CMakeFiles/CMakeDirectoryInformation.cmake would be reformatted
~/test-project/build/CMakeFiles/3.22.1/CMakeCXXCompiler.cmake would be reformatted

I would like to have the option of ignoring certain files or directories, so that the build directory and any other CMake files generated at configure or build time could be ignored by gersemi. I believe hard-coding the auto-generated files them would not work very well, because the ExternalProject and FetchContent modules could put any CMake file in the build directory.

No custom formatter for `exec_program()`

There is no custom formatter for exec_program(), so the arguments are not indented at all.

For instance, this input remains unchanged:

macro(GET_PROJECT_GIT_VERSION)
    find_package(Git QUIET)
    if(Git_FOUND)
        message(STATUS "Looking for git-versioning information.")
        exec_program(
            ${GIT_EXECUTABLE}
            ${CMAKE_CURRENT_SOURCE_DIR}
            ARGS
            describe
            --tags
            --match
            'v[0-9]*.*.*'
            --dirty
            OUTPUT_VARIABLE
            GIT_VERSION_INFO
            RETURN_VALUE
            GIT_ERROR
        )
    endif()
endmacro()

I would expect ARGS to be indented (muti-value option) and OUTPUT_VARIABLE and RETURN_VALUE to pull their single argument onto their own line.

Gersemi does not seem to accept arguments when used via pre-commit

  - repo: https://github.com/BlankSpruce/gersemi  # Formatter
    rev: 0.12.1
    hooks:
      - id: gersemi
        args: [--list-expansion, favour-inlining, --indent, '4']

I would like to customize the behaviour of gersemi in my pre-commit hooks. It looks as if putting anything in the args makes gersemi doing nothing. Probably it displays some message, presumably non-error, as returning non-zero code would be cought by the pre-commit.

I have no problem with passing arguments to other pre-commit hooks I use.

P.S. It is enough to reproduce the error even by empty args property, like this:

  - repo: https://github.com/BlankSpruce/gersemi  # Formatter
    rev: 0.12.1
    hooks:
      - id: gersemi
        args: []

@DanielChabrowski

Nondeterminism with conflicting definitions

If I give Gersemi conflicting definitions of a function, Gersemi (0.9.2) formats the calls to it nondeterministically, in accordance with either one or the other definition.

That's of course an invalid configuration, but I suppose as a feature request, it would be useful if Gersemi could detect this, blame the user and fail in such a case. This because not every CMake contributor is going to be aware of this pitfall (speaking firstly about myself here).

Example:

# cmake/poem_parsearg_edition.cmake
function(poem)
    cmake_parse_arguments("ARG" "" "" "LINES" ${ARGN})
    message(FATAL_ERROR ${ARG_LINES})
endfunction()
# cmake/poem_straightarg_edition.cmake
function(poem)
    message(FATAL_ERROR ${ARGN})
endfunction()
# CMakeLists.txt
poem(LINES "foo" "bar")
gersemi --list-expansion=favour-expansion --definitions cmake/ --diff CMakeLists.txt

Result 1:

-poem(LINES "foo" "bar")
+poem(
+    LINES
+    "foo"
+    "bar"
+)

Result 2:

-poem(LINES "foo" "bar")
+poem(
+    LINES
+        "foo"
+        "bar"
+)

Thank you for writing Gersemi! I could write long and wide about properties of good and bad formatters, but for one thing: Predictability. It was always a surprise where cmake-format would break the line, but Gersemi's format is very consistent and easy to follow, which I think is praiseworthy and underrated.

install command with multiple patterns

The following CMake code:

install(
  DIRECTORY "foobar"
  DESTINATION "/"
  PATTERN "something" EXCLUDE
  PATTERN "bin/*" PERMISSIONS
    OWNER_READ OWNER_WRITE OWNER_EXECUTE
    GROUP_READ GROUP_EXECUTE
    WORLD_READ WORLD_EXECUTE
  PATTERN "other-*"  PERMISSIONS
    OWNER_READ OWNER_WRITE OWNER_EXECUTE
    GROUP_READ GROUP_EXECUTE
    WORLD_READ WORLD_EXECUTE
)

Is formatted like this:

install(
    DIRECTORY "foobar"
    DESTINATION "/"
    PATTERN
        "something"
        EXCLUDE
        PATTERN
        "bin/*"
        PERMISSIONS
            OWNER_READ
            OWNER_WRITE
            OWNER_EXECUTE
            GROUP_READ
            GROUP_EXECUTE
            WORLD_READ
            WORLD_EXECUTE
            PATTERN
            "other-*"
        PERMISSIONS
            OWNER_READ
            OWNER_WRITE
            OWNER_EXECUTE
            GROUP_READ
            GROUP_EXECUTE
            WORLD_READ
            WORLD_EXECUTE
)

Instead of something like this:

install(
    DIRECTORY "foobar"
    DESTINATION "/"
    PATTERN "something" EXCLUDE
    PATTERN "bin/*" PERMISSIONS
        OWNER_READ
        OWNER_WRITE
        OWNER_EXECUTE
        GROUP_READ
        GROUP_EXECUTE
        WORLD_READ
        WORLD_EXECUTE
    PATTERN "other-*" PERMISSIONS
        OWNER_READ
        OWNER_WRITE
        OWNER_EXECUTE
        GROUP_READ
        GROUP_EXECUTE
        WORLD_READ
        WORLD_EXECUTE
)

All PATTERN/REGEX should be on the same indentation level. Extra nice would be if permissions were formatted with OWNER_* on one line, GROUP_* on the next and finally WORLD_*.

empty list of libraries to link with a target causes gersemi crash

description

Although technically empty list of libraries to link with a target makes no sense, it's apparently a legal cmake syntax.
gersemi should not crash.

version

gersemi 0.9.4
lark 1.1.8
Python 3.9.18 (main, Oct 24 2023, 14:13:49) 

how to reproduce

$ cat CMakeLists.txt
target_link_libraries(foo)
$ gersemi CMakeLists.txt

outcome

multiprocessing.pool.RemoteTraceback: 
"""
Traceback (most recent call last):
  File "/usr/lib/python3.9/multiprocessing/pool.py", line 125, in worker
    result = (True, func(*args, **kwds))
  File "/usr/lib/python3.9/multiprocessing/pool.py", line 48, in mapstar
    return list(map(*args))
  File "/tmp/.venv/lib/python3.9/site-packages/gersemi/runner.py", line 126, in run_task
    to_stderr=get_error_message(formatted_file),
  File "/tmp/.venv/lib/python3.9/site-packages/gersemi/result.py", line 36, in get_error_message
    exception, path = astuple(error)
  File "/usr/lib/python3.9/dataclasses.py", line 1140, in astuple
    return _astuple_inner(obj, tuple_factory)
  File "/usr/lib/python3.9/dataclasses.py", line 1147, in _astuple_inner
    value = _astuple_inner(getattr(obj, f.name), tuple_factory)
  File "/usr/lib/python3.9/dataclasses.py", line 1167, in _astuple_inner
    return copy.deepcopy(obj)
  File "/usr/lib/python3.9/copy.py", line 172, in deepcopy
    y = _reconstruct(x, memo, *rv)
  File "/usr/lib/python3.9/copy.py", line 264, in _reconstruct
    y = func(*args)
TypeError: __init__() missing 2 required positional arguments: 'obj' and 'orig_exc'
"""

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/tmp/.venv/bin/gersemi", line 8, in <module>
    sys.exit(main())
  File "/tmp/.venv/lib/python3.9/site-packages/gersemi/__main__.py", line 173, in main
    sys.exit(run(mode, configuration, args.sources))
  File "/tmp/.venv/lib/python3.9/site-packages/gersemi/runner.py", line 189, in run
    results = [
  File "/tmp/.venv/lib/python3.9/site-packages/gersemi/runner.py", line 189, in <listcomp>
    results = [
  File "/usr/lib/python3.9/multiprocessing/pool.py", line 448, in <genexpr>
    return (item for chunk in result for item in chunk)
  File "/usr/lib/python3.9/multiprocessing/pool.py", line 870, in next
    raise value
TypeError: __init__() missing 2 required positional arguments: 'obj' and 'orig_exc'

Forced line break → AST mismatch after formatting

In this example, the command line was long, so I tried to break it up using forced line breaks (empty line comments) …

set(SRC
    src/file.c
)

add_custom_command(
    OUTPUT
        src.tar.zst
    DEPENDS
        ${SRC}
    COMMAND
        set -euo pipefail #
        && tar -c src | zstd --ultra -22 > src.tar.zst #
)

… but Gersemi 0.11.0 didn't like that:

CMakeLists.txt: AST mismatch after formatting

Changing the tab size back and forth does produce a different formatting than doing nothing.

I did play with the crazy tab indent of size 15 and noticed one thing:

After formatting this piece of code:

    checkgitversion("${CheckGitSetup_REPOSITORY_PATH}"  "${CheckGitSetup_TEMPLATE_FILE}"  "${CheckGitSetup_OUTPUT_FILE}"  "${CheckGitSetup_INCLUDE_HEADER}")

with the indent 15 and then back to 4 again, I arrived with a different string:

    checkgitversion("${CheckGitSetup_REPOSITORY_PATH}"             "${CheckGitSetup_TEMPLATE_FILE}"             "${CheckGitSetup_OUTPUT_FILE}"             "${CheckGitSetup_INCLUDE_HEADER}")

Support for Python 3.6? missing module dataclasses.

Hello,

I wanted to test your formatter on our codebase but we are currently running Python 3.6 and it's a bit complicated to roll out 3.7 everywhere. I changed the version in setup.py to make it install - but it fails with the following error:

Traceback (most recent call last):
  File "/home/tobias/.pyenv/versions/3.6.4/bin/gersemi", line 11, in <module>
    load_entry_point('gersemi', 'console_scripts', 'gersemi')()
  File "/home/tobias/.pyenv/versions/3.6.4/lib/python3.6/site-packages/pkg_resources/__init__.py", line 587, in load_entry_point
    return get_distribution(dist).load_entry_point(group, name)
  File "/home/tobias/.pyenv/versions/3.6.4/lib/python3.6/site-packages/pkg_resources/__init__.py", line 2800, in load_entry_point
    return ep.load()
  File "/home/tobias/.pyenv/versions/3.6.4/lib/python3.6/site-packages/pkg_resources/__init__.py", line 2431, in load
    return self.resolve()
  File "/home/tobias/.pyenv/versions/3.6.4/lib/python3.6/site-packages/pkg_resources/__init__.py", line 2437, in resolve
    module = __import__(self.module_name, fromlist=['__name__'], level=0)
  File "/home/tobias/code/gersemi/gersemi/__main__.py", line 5, in <module>
    from gersemi.configuration import (
  File "/home/tobias/code/gersemi/gersemi/configuration.py", line 2, in <module>
    from dataclasses import asdict, dataclass, fields
ModuleNotFoundError: No module named 'dataclasses'

I am unsure - but this doesn't seem to be a problem with the python version dependency? Is there a file missing in the repo or something else I missed.

I installed it with pip install -e . from the checked out repo.

Inconsistent formatting of set command with favour-inlining enabled

With current master branch of gersemi I noticed that even with favour-inlining enabled some invocations of the set command get split up into multiple lines.

In my case the following invocation is kept on a single line:

set(CCACHE_SUPPORT ON CACHE BOOL "Enable ccache support")

Whereas the following invocation is split into multiple lines:

set(
  uuid_token_lengths
  8
  4
  4
  4
  12
)

This happens even though the column limit is set to 120, so the invocation should fit into a single line just fine.

add_custom_target command formatting

Given this CMake code:

add_custom_target(foobar ${CMAKE_COMMAND} -E env FOO=bar dostuff COMMAND ${CMAKE_COMMAND} -E env BAR=foo stuffdo DEPENDS foo bar)

I would expect it to be formatted something like this:

add_custom_target(
    foobar
    ${CMAKE_COMMAND} -E env FOO=bar dostuff
    COMMAND ${CMAKE_COMMAND} -E env BAR=foo stuffdo
    DEPENDS foo bar
)

But it is formatted like this:

add_custom_target(
    foobar
    ${CMAKE_COMMAND}
    -E env FOO=bar dostuff
    COMMAND ${CMAKE_COMMAND} -E env BAR=foo stuffdo
    DEPENDS foo bar
)

ArgumentAwareCommandInvocationDumper is not aware

I was wondering why install() formats like this:

install(
    FILES
        ${CMAKE_CURRENT_BINARY_DIR}/CMConfig.cmake
        ${CMAKE_CURRENT_BINARY_DIR}/CMConfigVersion.cmake
    DESTINATION ${CMAKECONFIG_INSTALL_DIR}
)

(e.g. the multi-valued parameters for FILES are indented) but configure_package_config_file() formats like this:

configure_package_config_file(
    CMConfig.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/CMConfig.cmake
    INSTALL_DESTINATION
    ${CMAKECONFIG_INSTALL_DIR}
    PATH_VARS
    CMAKECONFIG_INSTALL_DIR
    FIND_MODULES_INSTALL_DIR
    MODULES_INSTALL_DIR
)

(e.g. the multi-values parameters for PATH_VARS are not indented). I would expect long(-ish) lists of arguments for multi-values parameters to be indented. The seven_samurai() example looks a bit weird, too: why are not the parameters for SHICHIROJI indented? (Or maybe they need to be longer, so that multi-line layout might kick in).

Inhibit favour expansion for project()?

This CMake code:

project(FooBar VERSION 1.2.3 LANGUAGES C CXX)

Is, with --list-expansion favour-expansion, formatted as:

project(
    FooBar
    VERSION 1.2.3
    LANGUAGES
        C
        CXX
)

To me it would look better if project in this case was formatted as if --list-expansion favour-expansion wasn't given. Or is that too much of a special case?

Inconsistent `add_library` indentation

I noticed that the add_library function gets formatted slightly differently depending on the labels:

add_library(
    foo
    INTERFACE
        include/File1.hpp
        include/File2.hpp
        include/File3.hpp
        include/File4.hpp
        include/File5.hpp
)

With many other labels this becomes something like

add_library(
    foo
    SHARED
    src/File1.cpp
    src/File2.cpp
    src/File3.cpp
    src/File4.cpp
    src/File5.cpp
)

I've tried with SHARED, STATIC, OBJECT and INTERFACE. INTERFACE seems the only one that is "correctly" formatted.

Is this intended?

Please update requirements to use PyYAML v6

Newest setup tools doesn't like using of deprecated metadata of PyYAML v5

  × Getting requirements to build wheel did not run successfully.
  │ exit code: 1
  ╰─> [62 lines of output]
      /tmp/pip-build-env-m9vryc7y/overlay/lib/python3.10/site-packages/setuptools/config/setupcfg.py:293: _DeprecatedConfig: Deprecated config in `setup.cfg`
      !!
      
              ********************************************************************************
              The license_file parameter is deprecated, use license_files instead.
      
              By 2023-Oct-30, you need to update your project and remove deprecated calls
              or your builds will no longer be supported.
      
              See https://setuptools.pypa.io/en/latest/userguide/declarative_config.html for details.
              ********************************************************************************
      
      !!
        parsed = self.parsers.get(option_name, lambda x: x)(value)
      running egg_info
      writing lib3/PyYAML.egg-info/PKG-INFO
      writing dependency_links to lib3/PyYAML.egg-info/dependency_links.txt
      writing top-level names to lib3/PyYAML.egg-info/top_level.txt

According to the issue yaml/pyyaml#536 it seems that PyYAML v5 won't be fixed, and new v6 should be used instead.

Function name casing

It would be nice if gersemi could be instructed to not always lower case function names, for cases where PascalCase is used instead of snake_case. E.g. official functions such as FetchContent_MakeAvailable() but also custom functions such as MyCoolFunction(). Either to maintain the casing used in the CMakeLists.txt or be able to enforce the casing used when the function is defined (or with a hint).

Formatting of find_package with multiple REQUIRED modules inconsistent

Take the following CMake line:

find_package(FFmpeg ${ffmpeg_version} REQUIRED avcodec avfilter avdevice avutil swscale avformat swresample)

I would have expected the formatting to produce code like this:

find_package(
  FFmpeg
  ${ffmpeg_version}
  REQUIRED
    avcodec
    avfilter
    avdevice
    avutil
    swscale
    avformat
    swresample
)

gersemi 0.12.0 is almost there, but doesn't indent the REQUIRED modules.

Gersemi does not provide formatted output when no changes were made

I've stumbled over a little problem with gersemi in comparison to clang-format, swift-format, and other formatters:

Those formatters always output the input file, representing the "formatted" variant even if no formatting changes where necessary. gersemi instead outputs nothing.

Context: I have a pre-commit hook that iterates over all changed files matching suffixes I have formatters for and effectively runs gersemi "${FILE}" | { diff -u "${FILE}" - || :; } | sed -e "1s|--- |--- a/|" -e "2s|+++ -|+++ b/${FILE}|" to generate a patch file with the changes.

Because gersemi does not output anything on a file that is correctly formatted, the stdin for the diff command is empty and the generated patch effectively deletes the entire file content.

IMO it would be beneficial if gersemi would behave like other code-formatters in this case.

Possible Issues with FindPackageStandardArgs

I gave this a try on a rather large project and I think I've found a small problem. Not sure if I'm misunderstanding what the format should look like, but I don't think the formatting of find_package_standard_args looks correct. Example output:

include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(
    Pylon
    REQUIRED_VARS
    PYLON_INCLUDE_DIR
    PYLON_BASE_LIBRARY
    PYLON_GC_BASE_LIBRARY
    PYLON_GENAPI_LIBRARY
    PYLON_UTILITY_LIBRARY
    # omitted for brevity
    FOUND_VAR
    PYLON_FOUND
    VERSION_VAR
    PYLON_VERSION
    FAIL_MESSAGE
    "Failed to find Pylon"
)

I would think something like this "should" be the output:

include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(
    Pylon
    REQUIRED_VARS
        PYLON_INCLUDE_DIR
        PYLON_BASE_LIBRARY
        PYLON_GC_BASE_LIBRARY
        PYLON_GENAPI_LIBRARY
        PYLON_UTILITY_LIBRARY
    # omitted for brevity
    FOUND_VAR PYLON_FOUND
    VERSION_VAR PYLON_VERSION
    FAIL_MESSAGE "Failed to find Pylon"
)

Is this an actual issue? Or do I need to point gersemi to the modules directory for my CMake installation (FindPackageStandardArgs is included with CMake).

Formatting of install command less readable than cmake-format

The install command is formatted by cmake-format the following way:

install(
  TARGETS ${target}
  EXPORT ${target}Targets
  RUNTIME DESTINATION "${OBS_EXECUTABLE_DESTINATION}"
          COMPONENT Development
          EXCLUDE_FROM_ALL
  LIBRARY DESTINATION "${OBS_LIBRARY_DESTINATION}"
          COMPONENT Development
          EXCLUDE_FROM_ALL
  ARCHIVE DESTINATION "${OBS_LIBRARY_DESTINATION}"
          COMPONENT Development
          EXCLUDE_FROM_ALL
  FRAMEWORK DESTINATION Frameworks
            COMPONENT Development
            EXCLUDE_FROM_ALL
  INCLUDES
  DESTINATION "${include_destination}"
  PUBLIC_HEADER
    DESTINATION "${include_destination}"
    COMPONENT Development
    EXCLUDE_FROM_ALL

With the exception of INCLUDES and PUBLIC_HEADER, the indentation follows the internal logic of the command, namely that RUNTIME/LIBRARY/ARCHIVE/etc. are specifiers for the possible type of target to install and DESTINATION/COMPONENT/etc. are immediately related to those.

gersemi just puts every parameter on the same indent level, but I would've expected something closer to this (which somewhat follows the indentation used for target sources when PUBLIC or PRIVATE are specified):

install(
  TARGETS <target>
  EXPORT <target>Targets
  RUNTIME
    DESTINATION <destination>
    COMPONENT <component>
    EXCLUDE_FROM_ALL
  LIBRARY
    DESTINATION <destination>
    COMPONENT <component>
    EXCLUDE_FROM_ALL
)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.