diff --git a/build.rst b/build.rst deleted file mode 100644 index 992a51b..0000000 --- a/build.rst +++ /dev/null @@ -1,46 +0,0 @@ -.. _targets: - -======= -Targets -======= - -The executables, libraries, and tests defined by a project are inferred from -scans of C++ sources. - -- Interface units (source files with an ``export module foo;`` decl) produce - static or shared libraries according to the value of ``BUILD_SHARED_LIBS``. - - - Interface units whose name ends in ``_`` produce non-installed ``OBJECT`` - libraries. This is useful for projects which produce multiple executables - with shared source but do not wish to expose the shared source as a library. - -- Implementation units (source files with a ``module foo;`` decl) are added as - source files to their interface unit's library target. - -- Sources which ``import executable;`` produce - executable targets. The name of the target is derived by stripping the source - file's name of extensions - the ``STEM`` of the source file. - -- Sources which ``import test_;`` produce - a test. By default, this will: - - - Create an executable target. - - Pass that executable to :cmake:`add_test() `. - - Link the test executable to ``gtest``. - - - If an interface unit of ``module test_:main`` is found then it will be linked - with each test executable, otherwise ``gtest_main`` will be linked. - - - If the command ``maud_add_test(source_file_path out_target_name)`` - is defined it will be invoked on each test source as it is scanned, allowing - you to override what a unit test is for your project. - -Other source files will not be automatically attached to any target. -Other target types require explicit cmake. For example, to produce a shared -library instead of a static library for ``module foo;`` you can write -``add_library(foo SHARED)`` in any included cmake module. This predefines the -target to which interface and implementation units of ``foo`` will be added. - -Directories named ``include`` are globbed up and added to ``INCLUDE_DIRECTORIES``, -so ``$project_root/subtool/include/subtool/foo.hxx`` can be included with -``#include "subtool/foo.hxx"`` from any header or source. diff --git a/cmake_modules/Maud.cmake b/cmake_modules/Maud.cmake index b303e35..83da164 100644 --- a/cmake_modules/Maud.cmake +++ b/cmake_modules/Maud.cmake @@ -456,7 +456,12 @@ function(_maud_scan source_file) message(VERBOSE " attaching to ${target_name}") set_property(TARGET ${target_name} APPEND PROPERTY MAUD_IMPORTS "${imports}") - set_target_properties(${target_name} PROPERTIES MAUD_SCANNED ON) + set_target_properties( + ${target_name} + PROPERTIES + MAUD_SCANNED ON + MAUD_MODULE "${module}" + ) target_compile_features( ${target_name} PUBLIC @@ -533,7 +538,7 @@ function(_maud_add_test source_file out_target_name) add_test(NAME test_.${name} COMMAND $ --gtest_brief=1) target_sources( test_.${name} - PRIVATE + PUBLIC FILE_SET module_providers TYPE CXX_MODULES ${_MAUD_BASE_DIRS} @@ -542,7 +547,6 @@ function(_maud_add_test source_file out_target_name) set_target_properties( test_.${name} PROPERTIES - MAUD_INTERFACE "${_MAUD_SELF_DIR}/test_.cxx" COMPILE_OPTIONS "${_MAUD_INCLUDE} ${_MAUD_SELF_DIR}/test_.hxx" ) endfunction() @@ -611,6 +615,10 @@ function(_maud_finalize_targets) if(NOT imports) set(imports "") endif() + get_target_property(module ${target} MAUD_MODULE) + if(module AND target_type STREQUAL "EXECUTABLE") + list(APPEND imports "${module}") + endif() message(VERBOSE " IMPORTS: ${imports}") # Link targets to imported modules @@ -629,10 +637,9 @@ function(_maud_finalize_targets) endforeach() get_target_property(interface ${target} MAUD_INTERFACE) - if(NOT interface) + if(NOT interface AND NOT TEST ${target}) if(target_type STREQUAL "EXECUTABLE") set(interface "${_MAUD_SELF_DIR}/executable.cxx") - set(source_access PRIVATE) else() get_target_property(src ${target} MAUD_INTERFACE_PARTITIONS) set(interface "${MAUD_DIR}/injected/${target}.cxx") @@ -644,18 +651,18 @@ function(_maud_finalize_targets) PROPERTIES MAUD_TYPE INTERFACE ) - set(source_access PUBLIC) message(VERBOSE " No primary interface supplied, injecting ${interface}") endif() target_sources( ${target} - ${source_access} + PUBLIC FILE_SET module_providers TYPE CXX_MODULES ${_MAUD_BASE_DIRS} FILES "${interface}" ) endif() + print_target_sources(${target}) if(TEST ${target} AND NOT COMMAND "maud_add_test") @@ -669,7 +676,7 @@ function(_maud_finalize_targets) target_sources( ${target} - PRIVATE + PUBLIC FILE_SET module_providers TYPE CXX_MODULES BASE_DIRS ${_MAUD_BASE_DIRS} diff --git a/index.rst b/index.rst index 9e0db53..5efe007 100644 --- a/index.rst +++ b/index.rst @@ -68,75 +68,6 @@ configuration options: Read more about :ref:`options`. -Preprocessing -------------- - -By default, maud uses a custom module scanner which ignores preprocessing -for efficiency and stops reading source files after the import declarations. -This works in the most common case where the preprocessor only encounters -``#include`` directives and an occasional ``#define``, which leaves -the module dependency graph unaffected. However it is possible for the -preprocessor to affect module and import declarations. For example: - -- an import declaration could be inside a conditional preprocessing block - -.. code-block:: cpp - - module foo; - #if BAR_VERSION >= 3 - import bar; - #endif - -- a set of import declarations could be included - -.. code-block:: cpp - - module foo; - #include "common_imports.hxx" - -- a macro could expand to a pragma directive which modifies an ``#include`` - -.. code-block:: cpp - - module; - #include "macros.hxx" - PUSH_INGORED_WARNING(-Wunused-variable); - #include "dodgy.hxx" - POP_INGORED_WARNING(); - module foo; - -- a macro could be used to derive the name of the module - -.. code-block:: cpp - - module; - #include "macros.hxx" - module PP_CAT(foo_, FOO_VERSION); - -(I'm actually not sure that the last two are even legal since a global -module fragment should exclusively contain preprocessing directives -:cxx20:`module.global.frag#1`, however clang allows both.) - -IMHO, it is not desirable to write interface blocks which depend on preprocessing. -Moreover C++26 will restrict usage of the preprocessor severely in module declarations -as described in `P3034R1 `_. - -.. _maud-preprocessing-scan-options: - -For source files which require it, the property ``MAUD_PREPROCESSING_SCAN_OPTIONS`` -can be set in cmake. This property should contain all compile options -necessary to correctly preprocess the source file, for example -``-I /home/i/foo/include -isystem /home/i/boost/include -DFOO_ENABLE_BAR=1``. - -Note that the output of these tools is in the JSON format described by `p1689 -`_ -and does not distinguish between a module implementation unit of ``foo`` from a -source file which happens to import ``foo``. Without information about which module -an implementation unit is associated with, it cannot be automatically added to -the corresponding target. As a workaround if you must have a preprocessing scan -of an implementation unit, you can split the implementation unit into partitions -whose primary module is exposed. - Built-in support for generated files ------------------------------------ diff --git a/modules.rst b/modules.rst index bb2c86b..cec94ab 100644 --- a/modules.rst +++ b/modules.rst @@ -1,11 +1,103 @@ -.. TODO introduction to C++20 modules +======= +Modules +======= + +.. default-domain:: c++ +.. highlight:: c++ + +C++20 introduced a new way to share declarations between translation units. +Instead of repeating declarations manually or implicitly through header inclusion, +they can be directly imported from a binary file. + +To define a module, write a :cxx20:`module interface unit `- +a C++ source file with an export module declaration: + +.. code-block:: + + export module foo; + export int foo_score() { return 3; } + export int doubled_foo_score(); + +Module interface units may export declarations. Those exported declarations will +be usable in any translation unit which imports the module: + +.. code-block:: + + import foo; + bool foo_score_is_critical() { return foo_score() > 5; } + +In addition to interface units, module implementation units can be written which +include definitions and implementation details for the declarations exported by +interface units: + +.. code-block:: + + module foo; + int doubled_foo_score() { return foo_score() * 2; } + +The details for how to compile source files into object files, libraries, and +executables are compiler-specific. For further information, consult the +`Clang `_, +`GCC `_, and +`Microsoft `_ +documentation on C++ modules. + +Module = library +~~~~~~~~~~~~~~~~ + +In a ``Maud`` project, we (mostly) make the extra stipulation that modules +correspond to libraries. This is *not* part of the C++ spec for modules but +it is a natural simplification for a build system, since it maps them onto +well-understood cmake objects. The way sources should be compiled is inferred +from the module declarations in the C++ sources: C++ sources are scanned as +part of the cmake configure step and their imports and exports enumerated. + +- For each module declared, a library will be built from all the interface + and implementation units of that module. +- For each module imported, a library will be linked- either one built by + your project or one discovered using + :cmake:`find_package() `. +- For each module declared, installation of its library will be configured + along with installation of its module interface units. This includes + ``find_package()``-compatible cmake scripts which will enable other + ``Maud`` based projects to find your library with no hand-written cmake; + just ``import``. + + .. note:: + + Modules whose name ends in ``_`` produce non-installed ``OBJECT`` + libraries. This is useful for projects which organize internal functions into + libraries, but require those not be installed. + +Executables are generated from each source which imports the special +module ``executable``. The executable's name the +:cmake:`STEM ` +(source file name, stripped of all extensions). + +:ref:`unit tests` are generated from sources which import the special +module ``test_``. + +Executables and tests can both be implementation units of a module. +This can be helpful when testing a module's non-exported declaration or when +an executable is a trivial front end to a module. + +Source files with no module declaration or special import will not be +automatically attached to any target, and other target types require +explicit cmake. To manipulate a target before ``Maud`` has attached +sources to it, you can write ``add_library(foo SHARED)`` in cmake. +Sources which declare ``module foo`` will still be attached to it. + +.. note:: + + Whether libraries are static or shared by default is controlled by + :cmake:`option(BUILD_SHARED_LIBS) ` Module partitions: ~~~~~~~~~~~~~~~~~~ Module partitions are a useful way to compartmentalize a module interface: -.. code-block:: cpp +.. code-block:: // foo-bar.cxx export module foo:bar; @@ -13,31 +105,120 @@ Module partitions are a useful way to compartmentalize a module interface: // foo-quux.cxx export module foo:quux; - export int foo_quux(); + import :bar; + export int foo_quux() { return foo_bar() + 1; } + + // foo-bar-impl.cxx + module foo; + int foo_bar() { return 0; } + +Partitioning your module interface is a purely internal organizational choice; +only whole partitions can be imported outside their module of origin. + +.. code-block:: - // foo.cxx export module foo; - export import :bar; - export import :quux; - // All exports from foo:bar and foo:quux are now exported from - // this, the primary module interface unit for foo. - -The primary module interface unit is required to ``export import`` every -partition which is a module interface unit :cxx20:`module.unit#3`, and if -you have written partitions then you probably don't have anything in the primary -module interface unit except those ``export import`` declarations. This feels -boilerplate-y, so if no primary module interface unit is detected then one will -be generated containing just those ``export import`` declarations. + // import bar:alpha; // ERROR! + import bar; // includes bar:alpha anyway + +If you want to define independently importable but still related modules, note that +module names may be dot-separated identifiers. ``export module foo.base;`` and +``export module foo.extensions;`` will produce two entirely distinct libraries. + +.. note:: + + The :cxx20:`standard ` requires that a **primary** module + interface unit must be provided with an ``export import`` declaration for + every partition interface unit. + + .. code-block:: + + export module foo; + export import :bar; + export import :quux; + + In a ``Maud`` project a correct primary module interface unit will be + generated if none is detected; if you have written partitions then you + *probably* don't require anything but those ``export import`` declarations + in your primary module interface unit. Questionable support: ~~~~~~~~~~~~~~~~~~~~~ -- Translation units other than module interface units are not necessarily reachable: - :cxx20:`module.reach#1` - Importing translation units other than necessarily reachable ones is implementation - defined. For example this includes importing a partition which is not an interface - unit. +- Translation units other than module interface units are not necessarily + :cxx20:`reachable ` and importing translation units other than + necessarily reachable ones is implementation defined. For example this includes + importing a partition which is not an interface unit. - As of this writing GCC 14 does not support ``module:private``. -- Header units are not currently supported. -- ``import std`` might be supported by your compiler, but maud does not guarantee it. +- Header units are not currently supported by ``Maud``. +- ``import std`` might be supported by your compiler, but ``Maud`` does not guarantee it. + +Preprocessing +~~~~~~~~~~~~~ + +By default, maud uses a custom module scanner which ignores preprocessing +for efficiency and stops reading source files after the import declarations. +This works in the most common case where the preprocessor only encounters +``#include`` directives and an occasional ``#define``, which leaves +the module dependency graph unaffected. However it is possible for the +preprocessor to affect module and import declarations. For example: + +- an import declaration could be inside a conditional preprocessing block + +.. code-block:: + + module foo; + #if BAR_VERSION >= 3 + import bar; + #endif + +- a set of import declarations could be included + +.. code-block:: + + module foo; + #include "common_imports.hxx" + +- a macro could expand to a pragma directive which modifies an ``#include`` + +.. code-block:: + + module; + #include "macros.hxx" + PUSH_INGORED_WARNING(-Wunused-variable); + #include "dodgy.hxx" + POP_INGORED_WARNING(); + module foo; + +- a macro could be used to derive the name of the module + +.. code-block:: + + module; + #include "macros.hxx" + module PP_CAT(foo_, FOO_VERSION); + +(I'm actually not sure that the last two are even legal since a global +module fragment should exclusively contain preprocessing directives +:cxx20:`module.global.frag#1`, however clang allows both.) + +IMHO, it is not desirable to write interface blocks which depend on preprocessing. +Moreover C++26 will restrict usage of the preprocessor severely in module declarations +as described in `P3034R1 `_. + +.. _maud-preprocessing-scan-options: + +For source files which require it, the property ``MAUD_PREPROCESSING_SCAN_OPTIONS`` +can be set in cmake. This property should contain all compile options +necessary to correctly preprocess the source file, for example +``-I /home/i/foo/include -isystem /home/i/boost/include -DFOO_ENABLE_BAR=1``. + +Note that the output of these tools is in the JSON format described by `p1689 +`_ +and does not distinguish between a module implementation unit of ``foo`` from a +source file which happens to import ``foo``. Without information about which module +an implementation unit is associated with, it cannot be automatically added to +the corresponding target. As a workaround if you must have a preprocessing scan +of an implementation unit, you can split the implementation unit into partitions +whose primary module is exposed. diff --git a/project.test.yaml b/project.test.yaml index c4d6deb..e62d3b0 100644 --- a/project.test.yaml +++ b/project.test.yaml @@ -253,6 +253,24 @@ disabling unit testing: - maud -DBUILD_TESTING=OFF +internal unit testing: +- write: foo.cxx + contents: | + export module foo:interface; + // not exported! + int foo_internal() { return 3; } +- write: internal.test.cxx + contents: | + module foo:some_test; + import :interface; + import test_; + + TEST_(can_access_internal) { + EXPECT_(foo_internal() == 3); + } +- maud --log-level=VERBOSE + + unit testing main: - write: test_main.cxx contents: | diff --git a/testing.rst b/testing.rst index ca435ba..c0c01e9 100644 --- a/testing.rst +++ b/testing.rst @@ -1,3 +1,5 @@ +.. _unit tests: + Unit tests ---------- @@ -14,8 +16,8 @@ usage differs. Instead of defining test suites explicitly with classes, one test suite is produced for each C++ source which includes -the special module declaration ``module test_``. Each test suite -is compiled into an executable target named ``test_.${SUITE_NAME}``. +the special import declaration ``import test_``. Each test suite +is compiled into an executable target named ``test_.${STEM}``. In a suite source file, two macros are included in the predefines buffer (an explicit ``#include`` is unnecessary): @@ -27,7 +29,7 @@ GTest is added to the include path, so explicit .. code-block:: c++ - module test_; + import test_; TEST_(basic) { int three = 3, five = 5; diff --git a/zen.rst b/zen.rst index 9b6f87a..bd6a03d 100644 --- a/zen.rst +++ b/zen.rst @@ -20,3 +20,10 @@ Zen :members: FIXME remove this when I figure out how to test sphinx better + + +FIXME where should this live + +Directories named ``include`` are globbed up and added to ``INCLUDE_DIRECTORIES``, +so ``$project_root/subtool/include/subtool/foo.hxx`` can be included with +``#include "subtool/foo.hxx"`` from any header or source.