Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,27 @@ release tags add a leading `v` to the package version.
Ninja as a warning because those tests then use the Makefile generator.

- Added the runnable `examples/cmake/` project, which builds one Fortran module
through every CMake discovery route -- scikit-build-core's entry point,
through every CMake discovery route -- scikit-build-core's entry points,
`CMAKE_MODULE_PATH`, `PRIK_DIR`, and an installation prefix -- with a script
that runs each route and calls the built extension.

- PRIK also publishes scikit-build-core's `cmake.root` entry point, which sets
`PRIK_ROOT`, so a project listing PRIK and `scikit-build-core>=0.11` in
`[build-system] requires` resolves
`find_package(PRIK CONFIG REQUIRED)` with no `PRIK_DIR`, `CMAKE_PREFIX_PATH`,
or `CMAKE_MODULE_PATH`. `include(UsePRIK)` keeps working through the existing
`cmake.module` entry point.

- A configure-time structural query that fails because PRIK or one of its
dependencies cannot be imported now names the selected `Python_EXECUTABLE`
and how to check it, while keeping the underlying error. Other generation
failures are reported unchanged.

- Added `prik doctor cmake`, which reports the imported package, the
distribution metadata answering for it, `cmake-dir`, `install-dir`, both
CMake entry points, and any duplicate installation or `PYTHONPATH` entry that
could answer instead.

- Array handles support allocatable and pointer arguments, results, module
variables, derived fields, optional arguments, and matching ordinary-array
parameters. Numeric and character arrays accept supported forward and
Expand Down
59 changes: 44 additions & 15 deletions docs/user/guide/cmake.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,15 @@ helper, and these routes differ only in how CMake reaches it:
| Packaged directory | `find_package(PRIK CONFIG REQUIRED)` | `-DPRIK_DIR="$(prik cmake-dir)"` |
| Installation prefix | `find_package(PRIK CONFIG REQUIRED)` | `-DCMAKE_PREFIX_PATH="$(prik install-dir)"` |
| Module path | `include(UsePRIK)` | `-DCMAKE_MODULE_PATH="$(prik cmake-dir)"` |
| scikit-build-core | `include(UsePRIK)` | nothing; the backend reads PRIK's `cmake.module` entry point |
| scikit-build-core | `find_package(PRIK CONFIG REQUIRED)` | nothing at all |

[`examples/cmake/`](../../../examples/cmake/README.md) is a runnable project
that builds the same module through every one of them, with a script that
checks each route in turn.

### Configuring a project yourself

The three command-line routes use whichever `prik` the shell resolves.
`PRIK_DIR` is package-specific, so setting it does not affect how other CMake
packages are found; `CMAKE_PREFIX_PATH` is the broader search path every
`find_package()` call shares.
Expand All @@ -84,29 +91,38 @@ for it. A source checkout installs nothing, and an editable install writes no
data files, so `install-dir` reports that instead of naming a prefix;
`cmake-dir` always answers.

### Packaging and pinned interpreters

For a [scikit-build-core](https://scikit-build-core.readthedocs.io/) wheel,
name PRIK as a build requirement:

```toml
[build-system]
requires = ["scikit-build-core>=0.10", "prik"]
requires = ["scikit-build-core>=0.11", "prik"]
build-backend = "scikit_build_core.build"
```

The backend then puts PRIK's packaged CMake directory on `CMAKE_MODULE_PATH`
itself, so `include(UsePRIK)` needs nothing on the command line and building
the wheel is one command:
The backend installs PRIK into its own build environment and reads PRIK's entry
points from there -- not the `prik` the shell resolves -- so the project keeps
the same `find_package(PRIK CONFIG REQUIRED)` it uses everywhere else, and
building the wheel takes no PRIK-specific argument. `cmake.root` arrived in
scikit-build-core 0.11, which is why that is the floor:

```bash
python3 -m pip wheel . --no-deps --wheel-dir dist
python3 -m pip wheel .
```

The three command-line routes above use whichever `prik` the shell resolves.
scikit-build-core instead uses the PRIK installed in its build environment,
which it finds through the `cmake.module` entry point. When the build must
match the interpreter CMake itself selected -- several environments on one
machine, or a `Python_EXECUTABLE` the project pins -- ask that interpreter,
which also needs no `-D` argument:
PRIK publishes both of scikit-build-core's discovery entry points, so either
project form works there with nothing on the command line:

| Entry point | What the backend sets | What the project calls |
| --- | --- | --- |
| `cmake.root` | `PRIK_ROOT` | `find_package(PRIK CONFIG REQUIRED)` |
| `cmake.module` | `CMAKE_MODULE_PATH` | `include(UsePRIK)` |

When the build must match the interpreter CMake itself selected -- several
environments on one machine, or a `Python_EXECUTABLE` the project pins -- ask
that interpreter, which also needs no `-D` argument:

```cmake
execute_process(
Expand All @@ -127,9 +143,14 @@ include(UsePRIK)
it generates, which is why that project configures with a plain
`cmake -S . -B build`.

[`examples/cmake/`](../../../examples/cmake/README.md) is a runnable project
that builds the same module through every route, with a script that checks each
one in turn.
### When a build finds no PRIK, or the wrong one

`prik doctor cmake` reports what a build system would discover: the imported
package, the distribution metadata answering for it, `cmake-dir`,
`install-dir`, both entry points, and any duplicate installation or
`PYTHONPATH` entry that could answer instead. Run it through the interpreter in
question -- `"${Python_EXECUTABLE}" -m prik doctor cmake` -- to see what CMake
sees.

## Common `prik_add_module()` options

Expand Down Expand Up @@ -195,6 +216,14 @@ language must be enabled because every PRIK extension contains generated C
binding code, and Fortran must be enabled whenever the module contributes
Fortran sources.

Select the two compilers from one vendor. A generated binding can include the
Fortran runtime's `ISO_Fortran_binding.h`, which a C compiler from another
vendor does not find: Apple Clang beside a Homebrew GNU Fortran fails to
compile the binding, while GNU `gcc` beside GNU Fortran resolves it. The
toolchain stays CMake's to choose, so name the pair through the usual
`CMAKE_C_COMPILER` and `CMAKE_Fortran_COMPILER`, or `CMAKE_ARGS` when a build
backend drives the configure step.

Normal Fortran sources and targets carry their link-language requirements
through CMake. For a raw archive or shared library whose language is otherwise
opaque, add `LINKER_LANGUAGE Fortran`; PRIK records that requirement in its
Expand Down
18 changes: 18 additions & 0 deletions docs/user/reference/cli-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ stages without building one, and two print paths another tool builds against.
python3 -m prik INPUT [INPUT ...] [BUILD OPTIONS]
python3 -m prik {parse,semantics,generate,probe} [OPTIONS] ...
python3 -m prik {cmake-dir,install-dir}
python3 -m prik doctor cmake
```

| Command | Purpose |
Expand All @@ -27,6 +28,7 @@ python3 -m prik {cmake-dir,install-dir}
| `probe` | Prints compiler-target datatype and ABI facts. |
| `cmake-dir` | Prints the directory holding PRIK's packaged CMake modules. |
| `install-dir` | Prints the prefix holding PRIK's installed data files. |
| `doctor` | Reports how a build system would discover this PRIK installation. |

## Getting help

Expand Down Expand Up @@ -309,6 +311,22 @@ cmake -S . -B build -DCMAKE_PREFIX_PATH="$(prik install-dir)"
Both make `find_package(PRIK CONFIG REQUIRED)` resolve. See the
[CMake builds guide](../guide/cmake.md) for the project side.

`doctor cmake` reports the same paths together with what resolved them, for
when a build finds no PRIK or the wrong one:

```bash
python3 -m prik doctor cmake
```

| Line | Reports |
| --- | --- |
| `prik version`, `imported package` | The version the metadata records, and the package directory actually imported. |
| `python executable` | The interpreter answering, which is the one CMake selected when the report is run through it. |
| `cmake-dir`, `install-dir` | The same paths those commands print, or why there is no prefix. |
| `distribution metadata` | Where the metadata answering for `prik` lives. |
| `entry point cmake.root`, `entry point cmake.module` | What a build backend would discover, by name and directory. |
| `conflicts` | Duplicate `prik` distributions, a `PYTHONPATH` entry holding another copy, or metadata that does not describe the imported package. |

## Compiler preprocessing

These options control preprocessing before parsing.
Expand Down
22 changes: 11 additions & 11 deletions examples/cmake/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ find_package(
REQUIRED
)

set(PRIK_DISCOVERY "include" CACHE STRING "How this project loads PRIK's CMake modules")
set_property(CACHE PRIK_DISCOVERY PROPERTY STRINGS include find-package)
set(PRIK_DISCOVERY "find-package" CACHE STRING "How this project loads PRIK's CMake modules")
set_property(CACHE PRIK_DISCOVERY PROPERTY STRINGS find-package include)

if(PRIK_DISCOVERY STREQUAL "find-package")
# Needs PRIK's CMake package on a search path: PRIK_DIR set to
# `prik cmake-dir`, or CMAKE_PREFIX_PATH set to `prik install-dir`.
find_package(PRIK CONFIG REQUIRED)
else()
# scikit-build-core adds PRIK's packaged module directory to
# CMAKE_MODULE_PATH through its cmake.module entry point, so this include
# needs no lookup. A plain configure gets there by setting
# CMAKE_MODULE_PATH to `prik cmake-dir`.
if(PRIK_DISCOVERY STREQUAL "include")
# The module-path route. A plain configure needs CMAKE_MODULE_PATH set to
# `prik cmake-dir`; scikit-build-core supplies it from PRIK's cmake.module
# entry point.
include(UsePRIK)
else()
# The default. scikit-build-core sets PRIK_ROOT from PRIK's cmake.root
# entry point, so this needs no argument there at all; a plain configure
# passes PRIK_DIR or CMAKE_PREFIX_PATH instead.
find_package(PRIK CONFIG REQUIRED)
endif()

prik_add_module(
Expand Down
24 changes: 22 additions & 2 deletions examples/cmake/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ and the full `prik_add_module()` surface.
| File | Role |
| --- | --- |
| [`kernel.f90`](kernel.f90) | Fortran module with one diffusion step and one reduction |
| [`CMakeLists.txt`](CMakeLists.txt) | One `prik_add_module()` call; `PRIK_DISCOVERY` selects `include` or `find-package` |
| [`CMakeLists.txt`](CMakeLists.txt) | One `prik_add_module()` call; `PRIK_DISCOVERY` selects `find-package` (default) or `include` |
| [`pyproject.toml`](pyproject.toml) | The same project as a scikit-build-core wheel |
| [`check_discovery_routes.sh`](check_discovery_routes.sh) | Builds and calls the extension once per route |

Expand All @@ -20,7 +20,12 @@ and the full `prik_add_module()` surface.
| `module-path` | `include(UsePRIK)` | `-DCMAKE_MODULE_PATH="$(prik cmake-dir)"` |
| `find-package-dir` | `find_package(PRIK CONFIG REQUIRED)` | `-DPRIK_DIR="$(prik cmake-dir)"` |
| `install-prefix` | `find_package(PRIK CONFIG REQUIRED)` | `-DCMAKE_PREFIX_PATH="$(prik install-dir)"` |
| `scikit-build-core` | `include(UsePRIK)` | nothing: the build backend reads PRIK's `cmake.module` entry point |
| `scikit-build-core` | `find_package(PRIK CONFIG REQUIRED)` | nothing: the backend sets `PRIK_ROOT` from PRIK's `cmake.root` entry point |

`find_package(PRIK CONFIG REQUIRED)` is the project's default here, so the
scikit-build-core route needs no argument at all. `PRIK_DISCOVERY=include`
selects `include(UsePRIK)` instead, which scikit-build-core also supports
through PRIK's `cmake.module` entry point.

## Requirements

Expand Down Expand Up @@ -81,3 +86,18 @@ conserved = heat.kernel.total(values) # 1.0

Scalar arguments take NumPy scalars, which is PRIK's ordinary calling
convention rather than anything specific to CMake builds.

## Diagnosing a route

`prik doctor cmake` reports what a build system would discover -- the imported
package, the metadata answering for it, both entry points, and anything that
could answer instead. Run it through the interpreter in question to see what
that environment offers:

```bash
PYTHONPATH=. python3 -m prik doctor cmake
```

The script's `scikit-build-core` route builds against this checkout with build
isolation off. The isolated build a user gets from `pip wheel .` is covered by
`tests/fortran/infrastructure/building/end_to_end/test_cmake_builds.py`.
2 changes: 1 addition & 1 deletion examples/cmake/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build-system]
requires = ["scikit-build-core>=0.10", "prik"]
requires = ["scikit-build-core>=0.11", "prik"]
build-backend = "scikit_build_core.build"

[project]
Expand Down
39 changes: 38 additions & 1 deletion prik/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"%(prog)s INPUT [INPUT ...] [BUILD OPTIONS]\n"
" %(prog)s {parse,semantics,generate,probe} [OPTIONS] ...\n"
" %(prog)s {cmake-dir,install-dir}\n"
" %(prog)s doctor cmake\n"
" %(prog)s --version"
)
_BUILD_USAGE = (
Expand Down Expand Up @@ -89,7 +90,8 @@
" generate Generate contracts or wrapper build files\n"
" probe Probe compiler-target datatype and ABI facts\n"
" cmake-dir Print the directory holding PRIK's packaged CMake modules\n"
" install-dir Print the prefix holding PRIK's installed data files"
" install-dir Print the prefix holding PRIK's installed data files\n"
" doctor Report how a build system would discover this PRIK"
)
_CLI_HELP_EPILOG = (
f"{_HELP_DIVIDER}\n\n"
Expand Down Expand Up @@ -2964,6 +2966,38 @@ def _run_path_command(args: argparse.Namespace, parser: argparse.ArgumentParser)
return 0


_DOCTOR_HELP_EPILOG = (
f"{_HELP_DIVIDER}\n\n"
" Report what a CMake build would discover:\n"
" prik doctor cmake\n\n"
" Ask a specific interpreter, the way CMake does:\n"
" /path/to/python -m prik doctor cmake"
)


def _doctor_parser(argv: list[str]) -> argparse.ArgumentParser:
parser = _new_cli_parser(
prog="python3 -m prik doctor",
usage="%(prog)s cmake",
description="Report how a build system would discover this PRIK installation.",
epilog=_DOCTOR_HELP_EPILOG,
argv=argv,
)
parser.set_defaults(command="doctor")
parser.add_argument("topic", choices=("cmake",), help="Diagnostic report to print")
return parser


def _run_doctor_command(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int:
"""Print the facts that decide which PRIK a CMake build uses."""
from prik.installation import cmake_discovery_report

del parser # The only topic is validated by the parser's choices.
for label, value in cmake_discovery_report().items():
print(f"{label}: {value}")
return 0


def _probe_parser(argv: list[str]) -> argparse.ArgumentParser:
parser = _new_cli_parser(
prog="python3 -m prik probe",
Expand Down Expand Up @@ -3069,6 +3103,7 @@ def _probe_parser(argv: list[str]) -> argparse.ArgumentParser:
"install-dir",
"Print the prefix holding PRIK's installed data files.",
),
"doctor": _doctor_parser,
}


Expand Down Expand Up @@ -3178,6 +3213,8 @@ def main(argv: list[str] | None = None) -> int:
return _run_probe_command(args, parser)
if args.command in {"cmake-dir", "install-dir"}:
return _run_path_command(args, parser)
if args.command == "doctor":
return _run_doctor_command(args, parser)
args.language = _resolve_language(args.paths, args.language, parser)
preprocessing = _build_preprocessing_config(args, parser)
print_limit = _validate_main_options(args, parser)
Expand Down
27 changes: 26 additions & 1 deletion prik/cmake_modules/UsePRIK.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,31 @@ function(_prik_append_cli_flags command option flags)
set(${command} "${_command}" PARENT_SCOPE)
endfunction()

function(_prik_report_planning_failure name error)
# A missing PRIK, or a missing PRIK dependency, fails the structural query
# before it reads anything, and says so as an import error. Only that case
# gets the environment hint: a real generation or validation error must
# reach the user as itself, not behind a dependency story.
set(_prik_hint "")
if(error MATCHES "ModuleNotFoundError|ImportError|No module named")
set(
_prik_hint
"\n\nPRIK and its Python dependencies must be importable by the interpreter"
" CMake selected. Check it with:\n"
" \"${Python_EXECUTABLE}\" -m prik --version\n"
"then install PRIK into that environment, or select an interpreter that has"
" it with -DPython_EXECUTABLE=/path/to/python."
)
string(JOIN "" _prik_hint ${_prik_hint})
endif()
message(
FATAL_ERROR
"PRIK structural planning failed for ${name}.\n"
"Python_EXECUTABLE: ${Python_EXECUTABLE}\n\n"
"${error}${_prik_hint}"
)
endfunction()

function(_prik_json_string_list output_variable json)
set(_prik_json_path ${ARGN})
string(JSON _prik_item_count ERROR_VARIABLE _prik_json_error LENGTH "${json}" ${_prik_json_path})
Expand Down Expand Up @@ -343,7 +368,7 @@ function(prik_add_module name)
ERROR_VARIABLE _prik_configure_error
)
if(NOT _prik_configure_result EQUAL 0)
message(FATAL_ERROR "PRIK structural planning failed for ${name}:\n${_prik_configure_error}")
_prik_report_planning_failure("${name}" "${_prik_configure_error}")
endif()

_prik_json_string_list(_prik_generated_sources "${_prik_plan_json}" generated_sources)
Expand Down
Loading
Loading