Skip to content
Open
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
111 changes: 104 additions & 7 deletions src/fpm/manifest/preprocess.f90
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -59,6 +80,7 @@ module fpm_manifest_preprocess
!> Properties
procedure :: is_cpp
procedure :: is_fypp
procedure :: is_external

end type preprocess_config_t

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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

Expand All @@ -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

Expand All @@ -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
Expand All @@ -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
173 changes: 173 additions & 0 deletions test/fpm_test/test_manifest.f90
Original file line number Diff line number Diff line change
Expand Up @@ -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) &
& ]

Expand Down Expand Up @@ -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