From 4b3b4f080c15dd31749573746f3092cb0a3507a7 Mon Sep 17 00:00:00 2001 From: Jatin Yadav Date: Mon, 23 Mar 2026 00:46:39 +0530 Subject: [PATCH] extend preprocess_config_t: remove CPP guard, add external preprocessor fields --- src/fpm/manifest/preprocess.f90 | 111 ++++++++++++++++++-- test/fpm_test/test_manifest.f90 | 173 ++++++++++++++++++++++++++++++++ 2 files changed, 277 insertions(+), 7 deletions(-) diff --git a/src/fpm/manifest/preprocess.f90 b/src/fpm/manifest/preprocess.f90 index 8cd2c3a016..a6db95103b 100644 --- a/src/fpm/manifest/preprocess.f90 +++ b/src/fpm/manifest/preprocess.f90 @@ -8,6 +8,13 @@ !> suffixes = ["F90", "f90"] !> directories = ["src/feature1", "src/models"] !> macros = [] +!> +!> [preprocess.fypp] +!> command = "fypp" +!> suffixes = [".fypp"] +!> macros = ["WITH_QP", "MAXRANK=4"] +!> arguments = ["--line-marker", "--strict"] +!> depfile = true !> ``` module fpm_manifest_preprocess @@ -37,6 +44,20 @@ module fpm_manifest_preprocess !> Macros to be defined for the preprocessor type(string_t), allocatable :: macros(:) + !> Command to invoke the preprocessor (e.g. "fypp", "cpp") + !> Presence of this field is the activation key for external execution. + character(len=:), allocatable :: command + + !> Additional CLI arguments passed to the preprocessor + type(string_t), allocatable :: arguments(:) + + !> Output file suffix (default: ".f90") + character(len=:), allocatable :: output_suffix + + !> Whether this tool supports --depfile for transitive include tracking. + !> Opt-in; default .false. so custom tools are not passed unsupported flags. + logical :: depfile = .false. + contains !> Print information on this instance @@ -59,6 +80,7 @@ module fpm_manifest_preprocess !> Properties procedure :: is_cpp procedure :: is_fypp + procedure :: is_external end type preprocess_config_t @@ -115,6 +137,15 @@ subroutine new_preprocess_config(self, table, error) call get_list(table, "macros", self%macros, error) if (allocated(error)) return + call get_value(table, "command", self%command) + + call get_list(table, "arguments", self%arguments, error) + if (allocated(error)) return + + call get_value(table, "output-suffix", self%output_suffix) + + call get_value(table, "depfile", self%depfile, .false.) + end subroutine new_preprocess_config !> Check local schema for allowed entries @@ -136,7 +167,7 @@ subroutine check(table, error) do ikey = 1, size(list) select case(list(ikey)%key) !> Valid keys. - case("suffixes", "directories", "macros") + case("suffixes", "directories", "macros", "command", "arguments", "output-suffix", "depfile") case default call syntax_error(error, "Key '"//list(ikey)%key//"' not allowed in preprocessor '"//name//"'."); exit end select @@ -224,6 +255,21 @@ subroutine info(self, unit, verbosity) write(unit, fmt) " - " // self%macros(ilink)%s end do end if + if (allocated(self%command)) then + write(unit, fmt) "- command", self%command + end if + if (allocated(self%arguments)) then + write(unit, fmt) " - arguments" + do ilink = 1, size(self%arguments) + write(unit, fmt) " - " // self%arguments(ilink)%s + end do + end if + if (allocated(self%output_suffix)) then + write(unit, fmt) "- output-suffix", self%output_suffix + end if + if (self%depfile) then + write(unit, fmt) "- depfile", "true" + end if end subroutine info @@ -246,6 +292,20 @@ logical function preprocess_is_same(this,that) if (.not.(this%directories==other%directories)) return if (.not.(this%macros==other%macros)) return + if (allocated(this%command).neqv.allocated(other%command)) return + if (allocated(this%command)) then + if (.not.(this%command==other%command)) return + endif + + if (.not.(this%arguments==other%arguments)) return + + if (allocated(this%output_suffix).neqv.allocated(other%output_suffix)) return + if (allocated(this%output_suffix)) then + if (.not.(this%output_suffix==other%output_suffix)) return + endif + + if (.not.(this%depfile.eqv.other%depfile)) return + class default ! Not the same type return @@ -276,6 +336,14 @@ subroutine dump_to_toml(self, table, error) if (allocated(error)) return call set_list(table, "macros", self%macros, error) if (allocated(error)) return + call set_string(table, "command", self%command, error) + if (allocated(error)) return + call set_list(table, "arguments", self%arguments, error) + if (allocated(error)) return + call set_string(table, "output-suffix", self%output_suffix, error) + if (allocated(error)) return + call set_value(table, "depfile", self%depfile, error, class_name) + if (allocated(error)) return end subroutine dump_to_toml @@ -298,6 +366,11 @@ subroutine load_from_toml(self, table, error) if (allocated(error)) return call get_list(table, "macros", self%macros, error) if (allocated(error)) return + call get_value(table, "command", self%command) + call get_list(table, "arguments", self%arguments, error) + if (allocated(error)) return + call get_value(table, "output-suffix", self%output_suffix) + call get_value(table, "depfile", self%depfile, .false.) end subroutine load_from_toml @@ -309,6 +382,10 @@ elemental subroutine destroy(this) if (allocated(this%suffixes))deallocate(this%suffixes) if (allocated(this%directories))deallocate(this%directories) if (allocated(this%macros))deallocate(this%macros) + if (allocated(this%command))deallocate(this%command) + if (allocated(this%arguments))deallocate(this%arguments) + if (allocated(this%output_suffix))deallocate(this%output_suffix) + this%depfile = .false. end subroutine destroy @@ -317,11 +394,7 @@ subroutine add_config(this,that) class(preprocess_config_t), intent(inout) :: this type(preprocess_config_t), intent(in) :: that - if (.not.that%is_cpp()) then - write(stderr, '(a)') 'Warning: Preprocessor ' // that%name // & - ' is not supported; will ignore it' - return - end if + ! Accept any preprocessor name (cpp, fypp, or user-defined) if (.not.allocated(this%name)) this%name = that%name @@ -337,6 +410,23 @@ subroutine add_config(this,that) if (allocated(that%directories)) & call add_strings(this%directories, that%directories) + ! Set command if provided + if (allocated(that%command)) then + if (.not.allocated(this%command)) this%command = that%command + end if + + ! Add arguments + if (allocated(that%arguments)) & + call add_strings(this%arguments, that%arguments) + + ! Set output_suffix if provided + if (allocated(that%output_suffix)) then + if (.not.allocated(this%output_suffix)) this%output_suffix = that%output_suffix + end if + + ! Set depfile if source enables it + if (that%depfile) this%depfile = .true. + end subroutine add_config ! Check cpp @@ -346,11 +436,18 @@ elemental logical function is_cpp(this) if (allocated(this%name)) is_cpp = this%name == "cpp" end function is_cpp - ! Check cpp + ! Check fypp logical function is_fypp(this) class(preprocess_config_t), intent(in) :: this is_fypp = .false. if (allocated(this%name)) is_fypp = this%name == "fypp" end function is_fypp + ! Check if this preprocessor requires external execution + ! Returns .true. when the command field is allocated (the activation key) + elemental logical function is_external(this) + class(preprocess_config_t), intent(in) :: this + is_external = allocated(this%command) + end function is_external + end module fpm_manifest_preprocess diff --git a/test/fpm_test/test_manifest.f90 b/test/fpm_test/test_manifest.f90 index 482e053dfb..f8f1335cfb 100644 --- a/test/fpm_test/test_manifest.f90 +++ b/test/fpm_test/test_manifest.f90 @@ -88,6 +88,8 @@ subroutine collect_manifest(testsuite) & new_unittest("macro-parsing", test_macro_parsing, should_fail=.false.), & & new_unittest("macro-parsing-dependency", & & test_macro_parsing_dependency, should_fail=.false.), & + & new_unittest("preprocess-external", test_preprocess_external), & + & new_unittest("preprocess-backward-compat", test_preprocess_backward_compat), & & new_unittest("features-demo-serialization", test_features_demo_serialization) & & ] @@ -1938,4 +1940,175 @@ subroutine test_dependency_profile_features_conflict(error) end subroutine test_dependency_profile_features_conflict + !> Test that a non-CPP preprocessor with all new fields is parsed correctly + subroutine test_preprocess_external(error) + use fpm_manifest_preprocess, only: preprocess_config_t + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(package_config_t) :: package + character(:), allocatable :: temp_file + integer :: unit + + allocate(temp_file, source=get_temp_filename()) + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'name = "example"', & + & '[preprocess]', & + & '[preprocess.fypp]', & + & 'suffixes = ["fypp"]', & + & 'command = "fypp"', & + & 'macros = ["WITH_QP", "MAXRANK=4"]', & + & 'arguments = ["--line-marker", "--strict"]', & + & 'output-suffix = ".f90"', & + & 'depfile = true' + close(unit) + + call get_package_data(package, temp_file, error) + + open(file=temp_file, newunit=unit) + close(unit, status='delete') + + if (allocated(error)) return + + if (.not.allocated(package%preprocess)) then + call test_failed(error, "Preprocessor is not present in package data") + return + end if + + if (size(package%preprocess) /= 1) then + call test_failed(error, "Number of preprocessors should be one") + return + end if + + ! Check name + if (package%preprocess(1)%name /= "fypp") then + call test_failed(error, "Preprocessor name should be 'fypp'") + return + end if + + ! Check command (activation key) + if (.not.allocated(package%preprocess(1)%command)) then + call test_failed(error, "Preprocessor command should be allocated") + return + end if + if (package%preprocess(1)%command /= "fypp") then + call test_failed(error, "Preprocessor command should be 'fypp'") + return + end if + + ! Check is_external + if (.not.package%preprocess(1)%is_external()) then + call test_failed(error, "Preprocessor with command should be external") + return + end if + + ! Check suffixes + if (.not.allocated(package%preprocess(1)%suffixes)) then + call test_failed(error, "Preprocessor suffixes should be allocated") + return + end if + if (size(package%preprocess(1)%suffixes) /= 1) then + call test_failed(error, "Number of suffixes should be one") + return + end if + + ! Check macros + if (.not.allocated(package%preprocess(1)%macros)) then + call test_failed(error, "Preprocessor macros should be allocated") + return + end if + if (size(package%preprocess(1)%macros) /= 2) then + call test_failed(error, "Number of macros should be two") + return + end if + + ! Check arguments + if (.not.allocated(package%preprocess(1)%arguments)) then + call test_failed(error, "Preprocessor arguments should be allocated") + return + end if + if (size(package%preprocess(1)%arguments) /= 2) then + call test_failed(error, "Number of arguments should be two") + return + end if + + ! Check output_suffix + if (.not.allocated(package%preprocess(1)%output_suffix)) then + call test_failed(error, "output_suffix should be allocated") + return + end if + if (package%preprocess(1)%output_suffix /= ".f90") then + call test_failed(error, "output_suffix should be '.f90'") + return + end if + + ! Check depfile + if (.not.package%preprocess(1)%depfile) then + call test_failed(error, "depfile should be .true.") + return + end if + + ! Test serialization roundtrip + call package%test_serialization('test_preprocess_external', error) + if (allocated(error)) return + + end subroutine test_preprocess_external + + + !> Verify that [preprocess.fypp] without command is silently ignored (backward compat) + subroutine test_preprocess_backward_compat(error) + use fpm_manifest_preprocess, only: preprocess_config_t + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(package_config_t) :: package + character(:), allocatable :: temp_file + integer :: unit + + allocate(temp_file, source=get_temp_filename()) + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'name = "example"', & + & '[preprocess]', & + & '[preprocess.fypp]', & + & 'suffixes = [".fypp"]', & + & 'macros = ["WITH_QP"]' + close(unit) + + call get_package_data(package, temp_file, error) + + open(file=temp_file, newunit=unit) + close(unit, status='delete') + + if (allocated(error)) return + + if (.not.allocated(package%preprocess)) then + call test_failed(error, "Preprocessor should be present") + return + end if + + ! command should NOT be allocated — no external execution triggered + if (allocated(package%preprocess(1)%command)) then + call test_failed(error, "command should not be allocated when not specified") + return + end if + + ! is_external() must return .false. + if (package%preprocess(1)%is_external()) then + call test_failed(error, "is_external() should be .false. without command field") + return + end if + + ! Test serialization roundtrip + call package%test_serialization('test_preprocess_backward_compat', error) + if (allocated(error)) return + + end subroutine test_preprocess_backward_compat + + end module test_manifest