diff --git a/.gitignore b/.gitignore index 2d7f34cea4100f7d12b45e9c108edeeed8362fc5..4a771af6845a1ca2582df80e21dd0cd1d605f372 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ # dev environment **/.venv **/venv +**/.nox # build artifacts dist diff --git a/docs/source/api/generation.rst b/docs/source/api/generation.rst index 36b5ca03e52dabb2301a2ff0b72f5b9624d32fbb..935e722452b2755a98bee98063de12c0cc84c54f 100644 --- a/docs/source/api/generation.rst +++ b/docs/source/api/generation.rst @@ -30,7 +30,3 @@ Categories, Parameter Types, and Special Values .. autoclass:: ClangFormatOptions :members: -Option Descriptors ------------------- - -.. autoclass:: Option diff --git a/docs/source/usage/generator_scripts.md b/docs/source/usage/generator_scripts.md index d4086027ee9e2ec9f4b0bad226aad363f814e040..4a1f6aa7c34ae4667b938d80fc8bd4b595050361 100644 --- a/docs/source/usage/generator_scripts.md +++ b/docs/source/usage/generator_scripts.md @@ -320,6 +320,7 @@ with the `--help` flag: $ python kernels.py --help ``` +(custom_cli_args)= ## Adding Custom Command-Line Options Sometimes, you might want to add your own command-line options to a generator script diff --git a/docs/source/usage/project_integration.md b/docs/source/usage/project_integration.md index de3aa0c57558de101d2a1a06f07be11d3d937b0e..47b1b6875a502f9c8011df1b51964a85b1ad6281 100644 --- a/docs/source/usage/project_integration.md +++ b/docs/source/usage/project_integration.md @@ -57,25 +57,59 @@ If you are using pystencils-sfg with CMake through the provided CMake module, ### Add the module -To include the module in your CMake source tree, a separate find module is provided. -You can use the global CLI to obtain the find module; simply run +To include the module in your CMake source tree, you must first add the pystencils-sfg *Find-module* +to your CMake module path. +To create the Find-module, navigate to the directory it should be placed in and run the following command: ```shell sfg-cli cmake make-find-module ``` -to create the file `FindPystencilsSfg.cmake` in the current directory. -Add it to the CMake module path, and load the *pystencils-sfg* module via *find_package*: +This will create the `FindPystencilsSfg.cmake` file. +Make sure that its containing directory is added to the CMake module path. + +To load pystencils-sfg into CMake, we first need to set the Python interpreter +of the environment SFG is installed in. +There are several ways of doing this: + +#### Set Python via a Find-Module Hint + +Set the `PystencilsSfg_PYTHON_PATH` hint variable inside your `CMakeLists.txt` to point at the +Python executable which should be used to invoke pystencils-sfg, e.g.: ```CMake -find_package( PystencilsSfg ) +set(PystencilsSfg_PYTHON_PATH ${CMAKE_SOURCE_DIR}/.venv/bin/python) +``` + +This is the recommended way, especially when other parts of your project also use Python. + +#### Set Python via a Cache Variable + +On the command line or in a [CMake configure preset](https://cmake.org/cmake/help/latest/manual/cmake-presets.7.html), +set the `PystencilsSfg_PYTHON_INTERPRETER` cache variable to point at the Python executable to be used to invoke pystencils-sfg; +e.g.: + +```bash +cmake -S . -B build -DPystencilsSfg_PYTHON_INTERPRETER=`pwd`/.venv/bin/python ``` -Make sure to set the `Python_ROOT_DIR` cache variable to point to the correct Python interpreter -(i.e. the virtual environment you have installed *pystencils-sfg* into). +If both the cache variable and the `PystencilsSfg_PYTHON_PATH` hint are set, the cache variable takes precedence, +so you can use the cache variable to override the hint. + +#### Automatically Find a Python Installation + +If none of the above is provided, pystencils-sfg will invoke [FindPython](https://cmake.org/cmake/help/latest/module/FindPython.html) +to determine the Python interpreter it should use. +You can affect this process through any of the hints listed in the `FindPython` documentation. + +#### Find pystencils-sfg + +Finally, call `find_package( PystencilsSfg )` from your `CMakeLists.txt` to load the SFG module. +If SFG as a dependency is not optional, add the `REQUIRED` flag such that the call will fail if +the package cannot be found. (cmake_add_generator_scripts)= -### Add generator scripts +### Adding Generator Scripts The primary interaction point in CMake is the function `pystencilssfg_generate_target_sources`, with the following signature: @@ -83,10 +117,12 @@ with the following signature: ```CMake pystencilssfg_generate_target_sources( <target> SCRIPTS script1.py [script2.py ...] + [SCRIPT_ARGS arg1 [arg2 ...]] [DEPENDS dependency1.py [dependency2.py...]] [FILE_EXTENSIONS <header-extension> <impl-extension>] [OUTPUT_MODE <standalone|inline|header-only>] [CONFIG_MODULE <path-to-config-module.py>] + [OUTPUT_DIRECTORY <output-directory>] ) ``` @@ -96,22 +132,32 @@ Any changes in the generator scripts, or any listed dependency, will trigger reg The function takes the following options: - `SCRIPTS`: A list of generator scripts + - `SCRIPT_ARGS`: A list of custom command line arguments passed to the generator scripts; see [](#custom_cli_args) - `DEPENDS`: A list of dependencies for the generator scripts - `FILE_EXTENSION`: The desired extensions for the generated files - `OUTPUT_MODE`: Sets the output mode of the code generator; see {any}`SfgConfig.output_mode`. - `CONFIG_MODULE`: Set the configuration module for all scripts registered with this call. If set, this overrides the value of `PystencilsSfg_CONFIG_MODULE` in the current scope (see [](#cmake_set_config_module)) + - `OUTPUT_DIRECTORY`: Custom output directory for generated files. If `OUTPUT_DIRECTORY` is a relative path, + it will be interpreted relative to the current build directory. -### Include generated files +If `OUTPUT_DIRECTORY` is *not* specified, any C++ header files generated by the above call +can be included in any files belonging to `target` via: -The `pystencils-sfg` CMake module creates a subfolder `sfg_sources/gen` at the root of the build tree -and writes all generated source files into it. The directory `sfg_sources` is added to the project's include -path, such that generated header files for a target `<target>` may be included via: ```C++ -#include "gen/<target>/kernels.h" +#include "gen/<file1.hpp>" +#include "gen/<file2.hpp>" +/* ... */ ``` +:::{attention} +If you change the code generator output directory using the `OUTPUT_DIRECTORY` argument, +you are yourself responsible for placing that directory--or any of its parents--on the +include path of your target. +::: + + (cmake_set_config_module)= ### Set a Configuration Module diff --git a/src/pystencilssfg/cmake/FindPystencilsSfg.cmake b/src/pystencilssfg/cmake/FindPystencilsSfg.cmake index a5e7b11d09ddb55da6802291a28be77d6a44f4f6..20a3fd596d99cf2db18e346609b8060bf86d32bc 100644 --- a/src/pystencilssfg/cmake/FindPystencilsSfg.cmake +++ b/src/pystencilssfg/cmake/FindPystencilsSfg.cmake @@ -1,16 +1,42 @@ -set( PystencilsSfg_FOUND OFF CACHE BOOL "pystencils source file generator found" ) +#[[ +Find-Module for pystencils-sfg. -mark_as_advanced( PystencilsSfg_FOUND ) +# Setting the Python interpreter -find_package( Python COMPONENTS Interpreter REQUIRED ) +If the cache entry PystencilsSfg_PYTHON_INTERPRETER is set, e.g. via the commandline +(`-DPystencilsSfg_PYTHON_INTERPRETER=<...>`), its value be taken as the Python interpreter +used to find and run pystencils-sfg. + +If the cache entry is unset, but the hint PystencilsSfg_PYTHON_PATH is set, its value will +be used as the Python interpreter. + +If none of these is set, a Python interpreter will be selected using the `FindPython` module. + +#]] + +if(NOT DEFINED CACHE{PystencilsSfg_PYTHON_INTERPRETER}) + # The Python interpreter cache variable is not set externally, so... + if(DEFINED PystencilsSfg_PYTHON_PATH) + # ... either initialize it from the hint variable ... + set( _sfg_cache_python_init ${PystencilsSfg_PYTHON_PATH} ) + else() + # ... or, if that is also unset, use the system Python + find_package( Python COMPONENTS Interpreter REQUIRED ) + set( _sfg_cache_python_init ${Python_EXECUTABLE} ) + endif() +endif() + +set(PystencilsSfg_PYTHON_INTERPRETER ${_sfg_cache_python_init} CACHE PATH "Path to the Python executable used to run pystencils-sfg") # Try to find pystencils-sfg in the python environment -execute_process(COMMAND ${Python_EXECUTABLE} -m pystencilssfg version --no-newline +execute_process(COMMAND ${PystencilsSfg_PYTHON_INTERPRETER} -m pystencilssfg version --no-newline RESULT_VARIABLE _PystencilsSfgFindResult OUTPUT_VARIABLE PystencilsSfg_VERSION ) if(${_PystencilsSfgFindResult} EQUAL 0) set( PystencilsSfg_FOUND ON ) +else() + set( PystencilsSfg_FOUND OFF ) endif() if(DEFINED PystencilsSfg_FIND_REQUIRED) @@ -21,8 +47,9 @@ endif() if(${PystencilsSfg_FOUND}) message( STATUS "Found pystencils Source File Generator (Version ${PystencilsSfg_VERSION})") + message( STATUS "Using Python interpreter ${PystencilsSfg_PYTHON_INTERPRETER} for SFG generator scripts.") - execute_process(COMMAND ${Python_EXECUTABLE} -m pystencilssfg cmake modulepath --no-newline + execute_process(COMMAND ${PystencilsSfg_PYTHON_INTERPRETER} -m pystencilssfg cmake modulepath --no-newline OUTPUT_VARIABLE _PystencilsSfg_CMAKE_MODULE_PATH) set( CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${_PystencilsSfg_CMAKE_MODULE_PATH}) diff --git a/src/pystencilssfg/cmake/modules/PystencilsSfg.cmake b/src/pystencilssfg/cmake/modules/PystencilsSfg.cmake index cd1f1baf2b8da061bd0596b76c28abf42d4fc5eb..0779599a0bd9dd1d90a5a1f1fd5f351f6a2fcde4 100644 --- a/src/pystencilssfg/cmake/modules/PystencilsSfg.cmake +++ b/src/pystencilssfg/cmake/modules/PystencilsSfg.cmake @@ -1,53 +1,62 @@ +#[[ +pystencils-sfg CMake module. -set(PystencilsSfg_GENERATED_SOURCES_DIR "${CMAKE_BINARY_DIR}/sfg_sources" CACHE PATH "Output directory for genenerated sources" ) +Do not include this module directly; instead use the CMake find-module of pystencils-sfg +to dynamically locate it. +#]] -mark_as_advanced(PystencilsSfg_GENERATED_SOURCES_DIR) -file(MAKE_DIRECTORY "${PystencilsSfg_GENERATED_SOURCES_DIR}") +# This cache variable definition is a duplicate of the one in FindPystencilsSfg.cmake +if(NOT DEFINED CACHE{PystencilsSfg_PYTHON_INTERPRETER}) + set(PystencilsSfg_PYTHON_INTERPRETER ${Python_EXECUTABLE} CACHE PATH "Path to the Python executable used to run pystencils-sfg") +endif() -function(_pssfg_add_gen_source target script) +if(NOT DEFINED CACHE{_Pystencils_Include_Dir}) + execute_process( + COMMAND ${PystencilsSfg_PYTHON_INTERPRETER} -c "from pystencils.include import get_pystencils_include_path; print(get_pystencils_include_path(), end='')" + OUTPUT_VARIABLE _pystencils_includepath_result + ) + set(_Pystencils_Include_Dir ${_pystencils_includepath_result} CACHE PATH "") +endif() + +function(_pssfg_add_gen_source target script outputDirectory) set(options) set(oneValueArgs) - set(multiValueArgs GENERATOR_ARGS DEPENDS) + set(multiValueArgs GENERATOR_ARGS USER_ARGS DEPENDS) cmake_parse_arguments(_pssfg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - set(generatedSourcesDir ${PystencilsSfg_GENERATED_SOURCES_DIR}/gen/${target}) get_filename_component(basename ${script} NAME_WLE) cmake_path(ABSOLUTE_PATH script OUTPUT_VARIABLE scriptAbsolute) - execute_process(COMMAND ${Python_EXECUTABLE} -m pystencilssfg list-files "--sep=;" --no-newline ${_pssfg_GENERATOR_ARGS} ${script} + execute_process(COMMAND ${PystencilsSfg_PYTHON_INTERPRETER} -m pystencilssfg list-files "--sep=;" --no-newline ${_pssfg_GENERATOR_ARGS} ${script} OUTPUT_VARIABLE generatedSources RESULT_VARIABLE _pssfg_result ERROR_VARIABLE _pssfg_stderr) - execute_process(COMMAND ${Python_EXECUTABLE} -c "from pystencils.include import get_pystencils_include_path; print(get_pystencils_include_path(), end='')" - OUTPUT_VARIABLE _Pystencils_INCLUDE_DIR) - if(NOT (${_pssfg_result} EQUAL 0)) message( FATAL_ERROR ${_pssfg_stderr} ) endif() set(generatedSourcesAbsolute) foreach (filename ${generatedSources}) - list(APPEND generatedSourcesAbsolute "${generatedSourcesDir}/${filename}") + list(APPEND generatedSourcesAbsolute "${outputDirectory}/${filename}") endforeach () - file(MAKE_DIRECTORY "${generatedSourcesDir}") + file(MAKE_DIRECTORY ${outputDirectory}) add_custom_command(OUTPUT ${generatedSourcesAbsolute} DEPENDS ${scriptAbsolute} ${_pssfg_DEPENDS} - COMMAND ${Python_EXECUTABLE} ${scriptAbsolute} ${_pssfg_GENERATOR_ARGS} - WORKING_DIRECTORY "${generatedSourcesDir}") + COMMAND ${PystencilsSfg_PYTHON_INTERPRETER} ${scriptAbsolute} ${_pssfg_GENERATOR_ARGS} ${_pssfg_USER_ARGS} + WORKING_DIRECTORY "${outputDirectory}") target_sources(${target} PRIVATE ${generatedSourcesAbsolute}) - target_include_directories(${target} PRIVATE ${PystencilsSfg_GENERATED_SOURCES_DIR} ${_Pystencils_INCLUDE_DIR}) endfunction() function(pystencilssfg_generate_target_sources TARGET) set(options) - set(oneValueArgs OUTPUT_MODE CONFIG_MODULE) - set(multiValueArgs SCRIPTS DEPENDS FILE_EXTENSIONS) + set(oneValueArgs OUTPUT_MODE CONFIG_MODULE OUTPUT_DIRECTORY) + set(multiValueArgs SCRIPTS DEPENDS FILE_EXTENSIONS SCRIPT_ARGS) cmake_parse_arguments(_pssfg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) set(generatorArgs) @@ -79,14 +88,39 @@ function(pystencilssfg_generate_target_sources TARGET) endif() endif() + if(DEFINED _pssfg_OUTPUT_DIRECTORY) + cmake_path(IS_RELATIVE _pssfg_OUTPUT_DIRECTORY _pssfg_output_dir_is_relative) + if(_pssfg_output_dir_is_relative) + set(outputDirectory ${CMAKE_CURRENT_BINARY_DIR}/${_pssfg_OUTPUT_DIRECTORY}) + else() + set(outputDirectory ${_pssfg_OUTPUT_DIRECTORY}) + endif() + else() + set(generatedSourcesIncludeDir ${CMAKE_CURRENT_BINARY_DIR}/_gen/${TARGET}) + set(outputDirectory ${generatedSourcesIncludeDir}/gen) + target_include_directories(${TARGET} PRIVATE ${generatedSourcesIncludeDir}) + endif() + if(DEFINED _pssfg_FILE_EXTENSIONS) string(JOIN "," extensionsString ${_pssfg_FILE_EXTENSIONS}) list(APPEND generatorArgs "--sfg-file-extensions=${extensionsString}") endif() + if(DEFINED _pssfg_SCRIPT_ARGS) + # User has provided custom command line arguments + set(userArgs ${_pssfg_SCRIPT_ARGS}) + endif() + foreach(codegenScript ${_pssfg_SCRIPTS}) - _pssfg_add_gen_source(${TARGET} ${codegenScript} GENERATOR_ARGS ${generatorArgs} DEPENDS ${_pssfg_DEPENDS}) + _pssfg_add_gen_source( + ${TARGET} ${codegenScript} ${outputDirectory} + GENERATOR_ARGS ${generatorArgs} + USER_ARGS ${userArgs} + DEPENDS ${_pssfg_DEPENDS} + ) endforeach() + + target_include_directories(${TARGET} PRIVATE ${_Pystencils_Include_Dir}) endfunction() diff --git a/src/pystencilssfg/config.py b/src/pystencilssfg/config.py index 3b858a5d668713e0c433b87d8f893047feade0cf..aae9dab541f95c2d7af09da46bce1346150bb4a3 100644 --- a/src/pystencilssfg/config.py +++ b/src/pystencilssfg/config.py @@ -77,9 +77,9 @@ class ClangFormatOptions(ConfigBase): """Options affecting the invocation of ``clang-format`` for automatic code formatting.""" code_style: BasicOption[str] = BasicOption("file") - """Code style to be used by clang-format. Passed verbatim to `--style` argument of the clang-format CLI. + """Code style to be used by clang-format. Passed verbatim to ``--style`` argument of the clang-format CLI. - Similar to clang-format itself, the default value is `file`, such that a `.clang-format` file found in the build + Similar to clang-format itself, the default value is ``file``, such that a ``.clang-format`` file found in the build tree will automatically be used. """ diff --git a/tests/integration/cmake_project/CMakeLists.txt b/tests/integration/cmake_project/CMakeLists.txt index efee91238eec893bc523f5df4710b0d40125a2b2..ee7afae6d5781df607246813f47328bc1d45cfd3 100644 --- a/tests/integration/cmake_project/CMakeLists.txt +++ b/tests/integration/cmake_project/CMakeLists.txt @@ -5,6 +5,10 @@ list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}) find_package( PystencilsSfg REQUIRED ) +if(NOT ${PystencilsSfg_FOUND}) + message( FATAL_ERROR "PystencilsSfg_FOUND was not set even though find_package returned successfully. This is an error." ) +endif() + set( UseGlobalCfgModule OFF CACHE BOOL "Specify config module globally" ) set( UseLocalCfgModule OFF CACHE BOOL "Specify config module locally" ) @@ -26,3 +30,17 @@ else() SCRIPTS GenTest.py ) endif() + +pystencilssfg_generate_target_sources( + TestApp + SCRIPTS CliTest.py + SCRIPT_ARGS apples bananas unicorns + OUTPUT_MODE header-only +) + +pystencilssfg_generate_target_sources( + TestApp + SCRIPTS CustomDirTest.py + OUTPUT_DIRECTORY my-output + OUTPUT_MODE header-only +) diff --git a/tests/integration/cmake_project/CliTest.py b/tests/integration/cmake_project/CliTest.py new file mode 100644 index 0000000000000000000000000000000000000000..0456eb2bfbcaf9dd516710db19b9660e57e0b246 --- /dev/null +++ b/tests/integration/cmake_project/CliTest.py @@ -0,0 +1,6 @@ +from pystencilssfg import SourceFileGenerator + +with SourceFileGenerator(keep_unknown_argv=True) as sfg: + sfg.include("<string>") + for i, arg in enumerate(sfg.context.argv): + sfg.code(f"constexpr std::string arg{i} = \"{arg}\";") diff --git a/tests/integration/cmake_project/CustomDirTest.py b/tests/integration/cmake_project/CustomDirTest.py new file mode 100644 index 0000000000000000000000000000000000000000..d99e6de9d4ab03d3105780d00d038aff6cbd0d7d --- /dev/null +++ b/tests/integration/cmake_project/CustomDirTest.py @@ -0,0 +1,4 @@ +from pystencilssfg import SourceFileGenerator + +with SourceFileGenerator() as sfg: + sfg.code("#define NOTHING") diff --git a/tests/integration/cmake_project/TestApp.cpp b/tests/integration/cmake_project/TestApp.cpp index 7ceb98b42f40e6c91dddad76170b4654e14d4851..aefde8d7d70a7758f84fbd34d80fe9ad6f6ff7e6 100644 --- a/tests/integration/cmake_project/TestApp.cpp +++ b/tests/integration/cmake_project/TestApp.cpp @@ -1,4 +1,4 @@ -#include "gen/TestApp/GenTest.hpp" +#include "gen/GenTest.hpp" int main(void) { return int( gen::getValue() ); diff --git a/tests/integration/test_cmake.py b/tests/integration/test_cmake.py index 21091583114094e2639818ea6da01eb8db44b009..94853ac1b8143ff7149e5a268e086decfed1af61 100644 --- a/tests/integration/test_cmake.py +++ b/tests/integration/test_cmake.py @@ -9,7 +9,9 @@ CMAKE_PROJECT_DIRNAME = "cmake_project" CMAKE_PROJECT_DIR = THIS_DIR / CMAKE_PROJECT_DIRNAME -@pytest.mark.parametrize("config_source", [None, "UseGlobalCfgModule", "UseLocalCfgModule"]) +@pytest.mark.parametrize( + "config_source", [None, "UseGlobalCfgModule", "UseLocalCfgModule"] +) def test_cmake_project(tmp_path, config_source): obtain_find_module_cmd = ["sfg-cli", "cmake", "make-find-module"] @@ -30,8 +32,20 @@ def test_cmake_project(tmp_path, config_source): run_result = subprocess.run(run_cmd) if config_source is not None: - assert (tmp_path / "sfg_sources" / "gen" / "TestApp" / "GenTest.c++").exists() + assert (tmp_path / "_gen" / "TestApp" / "gen" / "GenTest.c++").exists() assert run_result.returncode == 31 else: - assert (tmp_path / "sfg_sources" / "gen" / "TestApp" / "GenTest.cpp").exists() + assert (tmp_path / "_gen" / "TestApp" / "gen" / "GenTest.cpp").exists() assert run_result.returncode == 42 + + cli_test_output = tmp_path / "_gen" / "TestApp" / "gen" / "CliTest.hpp" + assert cli_test_output.exists() + + content = cli_test_output.read_text() + assert 'arg0 = "apples";' in content + assert 'arg1 = "bananas";' in content + assert 'arg2 = "unicorns";' in content + + custom_dir_output = tmp_path / "my-output" / "CustomDirTest.hpp" + assert custom_dir_output.exists() + assert "#define NOTHING" in custom_dir_output.read_text()