aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSebastian Ehlert <28669218+awvwgk@users.noreply.github.com>2021-03-31 16:13:58 +0200
committerGitHub <noreply@github.com>2021-03-31 16:13:58 +0200
commitd9dc9f2ae5f196c15a7d35cddabc805c40ff86ce (patch)
tree6f61952c630b023edec391daae2747063703d489 /src
parent5422ec57f4081bf2225f5dde5cc07999bf8010f9 (diff)
downloadfpm-d9dc9f2ae5f196c15a7d35cddabc805c40ff86ce.tar.gz
fpm-d9dc9f2ae5f196c15a7d35cddabc805c40ff86ce.zip
Phase out Haskell fpm (#420)
- remove bootstrap directory from repository - remove stack-build from CI workflow - move Fortran fpm to project root - adjust install script and bootstrap instructions
Diffstat (limited to 'src')
-rw-r--r--src/fpm.f90467
-rw-r--r--src/fpm/cmd/install.f90176
-rw-r--r--src/fpm/cmd/new.f90652
-rw-r--r--src/fpm/cmd/update.f9068
-rw-r--r--src/fpm/dependency.f90821
-rw-r--r--src/fpm/error.f90128
-rw-r--r--src/fpm/git.f90263
-rw-r--r--src/fpm/installer.f90284
-rw-r--r--src/fpm/manifest.f90184
-rw-r--r--src/fpm/manifest/build.f90162
-rw-r--r--src/fpm/manifest/dependency.f90248
-rw-r--r--src/fpm/manifest/example.f90175
-rw-r--r--src/fpm/manifest/executable.f90186
-rw-r--r--src/fpm/manifest/install.f90108
-rw-r--r--src/fpm/manifest/library.f90142
-rw-r--r--src/fpm/manifest/package.f90435
-rw-r--r--src/fpm/manifest/test.f90175
-rw-r--r--src/fpm/toml.f90120
-rw-r--r--src/fpm/versioning.f90412
-rw-r--r--src/fpm_backend.f90262
-rw-r--r--src/fpm_command_line.f901140
-rw-r--r--src/fpm_compiler.f90333
-rw-r--r--src/fpm_environment.f90185
-rw-r--r--src/fpm_filesystem.f90612
-rw-r--r--src/fpm_model.f90293
-rw-r--r--src/fpm_source_parsing.f90480
-rw-r--r--src/fpm_sources.f90220
-rw-r--r--src/fpm_strings.f90924
-rw-r--r--src/fpm_targets.f90553
29 files changed, 10208 insertions, 0 deletions
diff --git a/src/fpm.f90 b/src/fpm.f90
new file mode 100644
index 0000000..31b68ff
--- /dev/null
+++ b/src/fpm.f90
@@ -0,0 +1,467 @@
+module fpm
+use fpm_strings, only: string_t, operator(.in.), glob, join, string_cat
+use fpm_backend, only: build_package
+use fpm_command_line, only: fpm_build_settings, fpm_new_settings, &
+ fpm_run_settings, fpm_install_settings, fpm_test_settings
+use fpm_dependency, only : new_dependency_tree
+use fpm_environment, only: run
+use fpm_filesystem, only: is_dir, join_path, number_of_rows, list_files, exists, basename
+use fpm_model, only: fpm_model_t, srcfile_t, show_model, &
+ FPM_SCOPE_UNKNOWN, FPM_SCOPE_LIB, FPM_SCOPE_DEP, &
+ FPM_SCOPE_APP, FPM_SCOPE_EXAMPLE, FPM_SCOPE_TEST
+use fpm_compiler, only: get_module_flags, is_unknown_compiler
+
+
+use fpm_sources, only: add_executable_sources, add_sources_from_dir
+use fpm_targets, only: targets_from_sources, resolve_module_dependencies, &
+ resolve_target_linking, build_target_t, build_target_ptr, &
+ FPM_TARGET_EXECUTABLE, FPM_TARGET_ARCHIVE
+use fpm_manifest, only : get_package_data, package_config_t
+use fpm_error, only : error_t, fatal_error
+use fpm_manifest_test, only : test_config_t
+use,intrinsic :: iso_fortran_env, only : stdin=>input_unit, &
+ & stdout=>output_unit, &
+ & stderr=>error_unit
+use fpm_manifest_dependency, only: dependency_config_t
+use, intrinsic :: iso_fortran_env, only: error_unit
+implicit none
+private
+public :: cmd_build, cmd_run
+public :: build_model, check_modules_for_duplicates
+
+contains
+
+
+subroutine build_model(model, settings, package, error)
+ ! Constructs a valid fpm model from command line settings and toml manifest
+ !
+ type(fpm_model_t), intent(out) :: model
+ type(fpm_build_settings), intent(in) :: settings
+ type(package_config_t), intent(in) :: package
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: i, j
+ type(package_config_t) :: dependency
+ character(len=:), allocatable :: manifest, lib_dir
+
+ logical :: duplicates_found = .false.
+ type(string_t) :: include_dir
+
+ model%package_name = package%name
+
+ allocate(model%include_dirs(0))
+ allocate(model%link_libraries(0))
+
+ call new_dependency_tree(model%deps, cache=join_path("build", "cache.toml"))
+ call model%deps%add(package, error)
+ if (allocated(error)) return
+
+ if(settings%compiler.eq.'')then
+ model%fortran_compiler = 'gfortran'
+ else
+ model%fortran_compiler = settings%compiler
+ endif
+
+ if (is_unknown_compiler(model%fortran_compiler)) then
+ write(*, '(*(a:,1x))') &
+ "<WARN>", "Unknown compiler", model%fortran_compiler, "requested!", &
+ "Defaults for this compiler might be incorrect"
+ end if
+ model%output_directory = join_path('build',basename(model%fortran_compiler)//'_'//settings%build_name)
+
+ call get_module_flags(model%fortran_compiler, &
+ & join_path(model%output_directory,model%package_name), &
+ & model%fortran_compile_flags)
+ model%fortran_compile_flags = settings%flag // model%fortran_compile_flags
+
+ allocate(model%packages(model%deps%ndep))
+
+ ! Add sources from executable directories
+ if (is_dir('app') .and. package%build%auto_executables) then
+ call add_sources_from_dir(model%packages(1)%sources,'app', FPM_SCOPE_APP, &
+ with_executables=.true., error=error)
+
+ if (allocated(error)) then
+ return
+ end if
+
+ end if
+ if (is_dir('example') .and. package%build%auto_examples) then
+ call add_sources_from_dir(model%packages(1)%sources,'example', FPM_SCOPE_EXAMPLE, &
+ with_executables=.true., error=error)
+
+ if (allocated(error)) then
+ return
+ end if
+
+ end if
+ if (is_dir('test') .and. package%build%auto_tests) then
+ call add_sources_from_dir(model%packages(1)%sources,'test', FPM_SCOPE_TEST, &
+ with_executables=.true., error=error)
+
+ if (allocated(error)) then
+ return
+ endif
+
+ end if
+ if (allocated(package%executable)) then
+ call add_executable_sources(model%packages(1)%sources, package%executable, FPM_SCOPE_APP, &
+ auto_discover=package%build%auto_executables, &
+ error=error)
+
+ if (allocated(error)) then
+ return
+ end if
+
+ end if
+ if (allocated(package%example)) then
+ call add_executable_sources(model%packages(1)%sources, package%example, FPM_SCOPE_EXAMPLE, &
+ auto_discover=package%build%auto_examples, &
+ error=error)
+
+ if (allocated(error)) then
+ return
+ end if
+
+ end if
+ if (allocated(package%test)) then
+ call add_executable_sources(model%packages(1)%sources, package%test, FPM_SCOPE_TEST, &
+ auto_discover=package%build%auto_tests, &
+ error=error)
+
+ if (allocated(error)) then
+ return
+ endif
+
+ endif
+
+ do i = 1, model%deps%ndep
+ associate(dep => model%deps%dep(i))
+ manifest = join_path(dep%proj_dir, "fpm.toml")
+
+ call get_package_data(dependency, manifest, error, &
+ apply_defaults=.true.)
+ if (allocated(error)) exit
+
+ model%packages(i)%name = dependency%name
+ if (.not.allocated(model%packages(i)%sources)) allocate(model%packages(i)%sources(0))
+
+ if (allocated(dependency%library)) then
+
+ if (allocated(dependency%library%source_dir)) then
+ lib_dir = join_path(dep%proj_dir, dependency%library%source_dir)
+ if (is_dir(lib_dir)) then
+ call add_sources_from_dir(model%packages(i)%sources, lib_dir, FPM_SCOPE_LIB, &
+ error=error)
+ if (allocated(error)) exit
+ end if
+ end if
+
+ if (allocated(dependency%library%include_dir)) then
+ do j=1,size(dependency%library%include_dir)
+ include_dir%s = join_path(dep%proj_dir, dependency%library%include_dir(j)%s)
+ if (is_dir(include_dir%s)) then
+ model%include_dirs = [model%include_dirs, include_dir]
+ end if
+ end do
+ end if
+
+ end if
+
+ if (allocated(dependency%build%link)) then
+ model%link_libraries = [model%link_libraries, dependency%build%link]
+ end if
+ end associate
+ end do
+ if (allocated(error)) return
+
+ if (settings%verbose) then
+ write(*,*)'<INFO> BUILD_NAME: ',settings%build_name
+ write(*,*)'<INFO> COMPILER: ',settings%compiler
+ write(*,*)'<INFO> COMPILER OPTIONS: ', model%fortran_compile_flags
+ write(*,*)'<INFO> INCLUDE DIRECTORIES: [', string_cat(model%include_dirs,','),']'
+ end if
+
+ ! Check for duplicate modules
+ call check_modules_for_duplicates(model, duplicates_found)
+ if (duplicates_found) then
+ error stop 'Error: One or more duplicate module names found.'
+ end if
+end subroutine build_model
+
+! Check for duplicate modules
+subroutine check_modules_for_duplicates(model, duplicates_found)
+ type(fpm_model_t), intent(in) :: model
+ integer :: maxsize
+ integer :: i,j,k,l,m,modi
+ type(string_t), allocatable :: modules(:)
+ logical :: duplicates_found
+ ! Initialise the size of array
+ maxsize = 0
+ ! Get number of modules provided by each source file of every package
+ do i=1,size(model%packages)
+ do j=1,size(model%packages(i)%sources)
+ if (allocated(model%packages(i)%sources(j)%modules_provided)) then
+ maxsize = maxsize + size(model%packages(i)%sources(j)%modules_provided)
+ end if
+ end do
+ end do
+ ! Allocate array to contain distinct names of modules
+ allocate(modules(maxsize))
+
+ ! Initialise index to point at start of the newly allocated array
+ modi = 1
+
+ ! Loop through modules provided by each source file of every package
+ ! Add it to the array if it is not already there
+ ! Otherwise print out warning about duplicates
+ do k=1,size(model%packages)
+ do l=1,size(model%packages(k)%sources)
+ if (allocated(model%packages(k)%sources(l)%modules_provided)) then
+ do m=1,size(model%packages(k)%sources(l)%modules_provided)
+ if (model%packages(k)%sources(l)%modules_provided(m)%s.in.modules(:modi-1)) then
+ write(error_unit, *) "Warning: Module ",model%packages(k)%sources(l)%modules_provided(m)%s, &
+ " in ",model%packages(k)%sources(l)%file_name," is a duplicate"
+ duplicates_found = .true.
+ else
+ modules(modi) = model%packages(k)%sources(l)%modules_provided(m)
+ modi = modi + 1
+ end if
+ end do
+ end if
+ end do
+ end do
+end subroutine check_modules_for_duplicates
+
+subroutine cmd_build(settings)
+type(fpm_build_settings), intent(in) :: settings
+type(package_config_t) :: package
+type(fpm_model_t) :: model
+type(build_target_ptr), allocatable :: targets(:)
+type(error_t), allocatable :: error
+
+integer :: i
+
+call get_package_data(package, "fpm.toml", error, apply_defaults=.true.)
+if (allocated(error)) then
+ print '(a)', error%message
+ error stop 1
+end if
+
+call build_model(model, settings, package, error)
+if (allocated(error)) then
+ print '(a)', error%message
+ error stop 1
+end if
+
+call targets_from_sources(targets,model,error)
+if (allocated(error)) then
+ print '(a)', error%message
+ error stop 1
+end if
+
+if(settings%list)then
+ do i=1,size(targets)
+ write(stderr,*) targets(i)%ptr%output_file
+ enddo
+else if (settings%show_model) then
+ call show_model(model)
+else
+ call build_package(targets,model)
+endif
+
+end subroutine
+
+subroutine cmd_run(settings,test)
+ class(fpm_run_settings), intent(in) :: settings
+ logical, intent(in) :: test
+
+ integer :: i, j, col_width
+ logical :: found(size(settings%name))
+ type(error_t), allocatable :: error
+ type(package_config_t) :: package
+ type(fpm_model_t) :: model
+ type(build_target_ptr), allocatable :: targets(:)
+ type(string_t) :: exe_cmd
+ type(string_t), allocatable :: executables(:)
+ type(build_target_t), pointer :: exe_target
+ type(srcfile_t), pointer :: exe_source
+ integer :: run_scope
+ character(len=:),allocatable :: line
+ logical :: toomany
+
+ call get_package_data(package, "fpm.toml", error, apply_defaults=.true.)
+ if (allocated(error)) then
+ print '(a)', error%message
+ error stop 1
+ end if
+
+ call build_model(model, settings%fpm_build_settings, package, error)
+ if (allocated(error)) then
+ print '(a)', error%message
+ error stop 1
+ end if
+
+ call targets_from_sources(targets,model,error)
+ if (allocated(error)) then
+ print '(a)', error%message
+ error stop 1
+ end if
+
+ if (test) then
+ run_scope = FPM_SCOPE_TEST
+ else
+ run_scope = merge(FPM_SCOPE_EXAMPLE, FPM_SCOPE_APP, settings%example)
+ end if
+
+ ! Enumerate executable targets to run
+ col_width = -1
+ found(:) = .false.
+ allocate(executables(0))
+ do i=1,size(targets)
+
+ exe_target => targets(i)%ptr
+
+ if (exe_target%target_type == FPM_TARGET_EXECUTABLE .and. &
+ allocated(exe_target%dependencies)) then
+
+ exe_source => exe_target%dependencies(1)%ptr%source
+
+ if (exe_source%unit_scope == run_scope) then
+
+ col_width = max(col_width,len(basename(exe_target%output_file))+2)
+
+ if (size(settings%name) == 0) then
+
+ exe_cmd%s = exe_target%output_file
+ executables = [executables, exe_cmd]
+
+ else
+
+ do j=1,size(settings%name)
+
+ if (glob(trim(exe_source%exe_name),trim(settings%name(j)))) then
+
+ found(j) = .true.
+ exe_cmd%s = exe_target%output_file
+ executables = [executables, exe_cmd]
+
+ end if
+
+ end do
+
+ end if
+
+ end if
+
+ end if
+
+ end do
+
+ ! Check if any apps/tests were found
+ if (col_width < 0) then
+ if (test) then
+ write(stderr,*) 'No tests to run'
+ else
+ write(stderr,*) 'No executables to run'
+ end if
+ stop
+ end if
+
+ ! Check all names are valid
+ ! or no name and found more than one file
+ toomany= size(settings%name).eq.0 .and. size(executables).gt.1
+ if ( any(.not.found) &
+ & .or. &
+ & ( (toomany .and. .not.test) .or. (toomany .and. settings%runner .ne. '') ) &
+ & .and. &
+ & .not.settings%list) then
+ line=join(settings%name)
+ if(line.ne.'.')then ! do not report these special strings
+ if(any(.not.found))then
+ write(stderr,'(A)',advance="no")'fpm::run<ERROR> specified names '
+ do j=1,size(settings%name)
+ if (.not.found(j)) write(stderr,'(A)',advance="no") '"'//trim(settings%name(j))//'" '
+ end do
+ write(stderr,'(A)') 'not found.'
+ write(stderr,*)
+ else if(settings%verbose)then
+ write(stderr,'(A)',advance="yes")'<INFO>when more than one executable is available'
+ write(stderr,'(A)',advance="yes")' program names must be specified.'
+ endif
+ endif
+
+ call compact_list_all()
+
+ if(line.eq.'.' .or. line.eq.' ')then ! do not report these special strings
+ stop
+ else
+ stop 1
+ endif
+
+ end if
+
+ call build_package(targets,model)
+
+ if (settings%list) then
+ call compact_list()
+ else
+
+ do i=1,size(executables)
+ if (exists(executables(i)%s)) then
+ if(settings%runner .ne. ' ')then
+ call run(settings%runner//' '//executables(i)%s//" "//settings%args,echo=settings%verbose)
+ else
+ call run(executables(i)%s//" "//settings%args,echo=settings%verbose)
+ endif
+ else
+ write(stderr,*)'fpm::run<ERROR>',executables(i)%s,' not found'
+ stop 1
+ end if
+ end do
+ endif
+ contains
+ subroutine compact_list_all()
+ integer, parameter :: LINE_WIDTH = 80
+ integer :: i, j, nCol
+ j = 1
+ nCol = LINE_WIDTH/col_width
+ write(stderr,*) 'Available names:'
+ do i=1,size(targets)
+
+ exe_target => targets(i)%ptr
+
+ if (exe_target%target_type == FPM_TARGET_EXECUTABLE .and. &
+ allocated(exe_target%dependencies)) then
+
+ exe_source => exe_target%dependencies(1)%ptr%source
+
+ if (exe_source%unit_scope == run_scope) then
+
+ write(stderr,'(A)',advance=(merge("yes","no ",modulo(j,nCol)==0))) &
+ & [character(len=col_width) :: basename(exe_target%output_file)]
+ j = j + 1
+
+ end if
+ end if
+ end do
+ write(stderr,*)
+ end subroutine compact_list_all
+
+ subroutine compact_list()
+ integer, parameter :: LINE_WIDTH = 80
+ integer :: i, j, nCol
+ j = 1
+ nCol = LINE_WIDTH/col_width
+ write(stderr,*) 'Matched names:'
+ do i=1,size(executables)
+ write(stderr,'(A)',advance=(merge("yes","no ",modulo(j,nCol)==0))) &
+ & [character(len=col_width) :: basename(executables(i)%s)]
+ j = j + 1
+ enddo
+ write(stderr,*)
+ end subroutine compact_list
+
+end subroutine cmd_run
+
+end module fpm
diff --git a/src/fpm/cmd/install.f90 b/src/fpm/cmd/install.f90
new file mode 100644
index 0000000..db7a9f8
--- /dev/null
+++ b/src/fpm/cmd/install.f90
@@ -0,0 +1,176 @@
+module fpm_cmd_install
+ use, intrinsic :: iso_fortran_env, only : output_unit
+ use fpm, only : build_model
+ use fpm_backend, only : build_package
+ use fpm_command_line, only : fpm_install_settings
+ use fpm_error, only : error_t, fatal_error
+ use fpm_filesystem, only : join_path, list_files
+ use fpm_installer, only : installer_t, new_installer
+ use fpm_manifest, only : package_config_t, get_package_data
+ use fpm_model, only : fpm_model_t, FPM_SCOPE_APP
+ use fpm_targets, only: targets_from_sources, build_target_t, &
+ build_target_ptr, FPM_TARGET_EXECUTABLE
+ use fpm_strings, only : string_t, resize
+ implicit none
+ private
+
+ public :: cmd_install
+
+contains
+
+ !> Entry point for the fpm-install subcommand
+ subroutine cmd_install(settings)
+ !> Representation of the command line settings
+ type(fpm_install_settings), intent(in) :: settings
+ type(package_config_t) :: package
+ type(error_t), allocatable :: error
+ type(fpm_model_t) :: model
+ type(build_target_ptr), allocatable :: targets(:)
+ type(installer_t) :: installer
+ character(len=:), allocatable :: lib, exe, dir
+ logical :: installable
+
+ call get_package_data(package, "fpm.toml", error, apply_defaults=.true.)
+ call handle_error(error)
+
+ call build_model(model, settings%fpm_build_settings, package, error)
+ call handle_error(error)
+
+ call targets_from_sources(targets,model,error)
+ call handle_error(error)
+
+ installable = (allocated(package%library) .and. package%install%library) &
+ .or. allocated(package%executable)
+ if (.not.installable) then
+ call fatal_error(error, "Project does not contain any installable targets")
+ call handle_error(error)
+ end if
+
+ if (settings%list) then
+ call install_info(output_unit, package, model, targets)
+ return
+ end if
+
+ if (.not.settings%no_rebuild) then
+ call build_package(targets,model)
+ end if
+
+ call new_installer(installer, prefix=settings%prefix, &
+ bindir=settings%bindir, libdir=settings%libdir, &
+ includedir=settings%includedir, &
+ verbosity=merge(2, 1, settings%verbose))
+
+ if (allocated(package%library) .and. package%install%library) then
+ dir = join_path(model%output_directory, model%package_name)
+ lib = "lib"//model%package_name//".a"
+ call installer%install_library(join_path(dir, lib), error)
+ call handle_error(error)
+
+ call install_module_files(installer, dir, error)
+ call handle_error(error)
+ end if
+
+ if (allocated(package%executable)) then
+ call install_executables(installer, targets, error)
+ call handle_error(error)
+ end if
+
+ end subroutine cmd_install
+
+ subroutine install_info(unit, package, model, targets)
+ integer, intent(in) :: unit
+ type(package_config_t), intent(in) :: package
+ type(fpm_model_t), intent(in) :: model
+ type(build_target_ptr), intent(in) :: targets(:)
+
+ integer :: ii, ntargets
+ character(len=:), allocatable :: lib
+ type(string_t), allocatable :: install_target(:)
+
+ call resize(install_target)
+
+ ntargets = 0
+ if (allocated(package%library) .and. package%install%library) then
+ ntargets = ntargets + 1
+ lib = join_path(model%output_directory, model%package_name, &
+ "lib"//model%package_name//".a")
+ install_target(ntargets)%s = lib
+ end if
+ do ii = 1, size(targets)
+ if (is_executable_target(targets(ii)%ptr)) then
+ if (ntargets >= size(install_target)) call resize(install_target)
+ ntargets = ntargets + 1
+ install_target(ntargets)%s = targets(ii)%ptr%output_file
+ end if
+ end do
+
+ write(unit, '("#", *(1x, g0))') &
+ "total number of installable targets:", ntargets
+ do ii = 1, ntargets
+ write(unit, '("-", *(1x, g0))') install_target(ii)%s
+ end do
+
+ end subroutine install_info
+
+ subroutine install_module_files(installer, dir, error)
+ type(installer_t), intent(inout) :: installer
+ character(len=*), intent(in) :: dir
+ type(error_t), allocatable, intent(out) :: error
+ type(string_t), allocatable :: modules(:)
+ integer :: ii
+
+ call list_files(dir, modules, recurse=.false.)
+
+ do ii = 1, size(modules)
+ if (is_module_file(modules(ii)%s)) then
+ call installer%install_header(modules(ii)%s, error)
+ if (allocated(error)) exit
+ end if
+ end do
+ if (allocated(error)) return
+
+ end subroutine install_module_files
+
+ subroutine install_executables(installer, targets, error)
+ type(installer_t), intent(inout) :: installer
+ type(build_target_ptr), intent(in) :: targets(:)
+ type(error_t), allocatable, intent(out) :: error
+ integer :: ii
+
+ do ii = 1, size(targets)
+ if (is_executable_target(targets(ii)%ptr)) then
+ call installer%install_executable(targets(ii)%ptr%output_file, error)
+ if (allocated(error)) exit
+ end if
+ end do
+ if (allocated(error)) return
+
+ end subroutine install_executables
+
+ elemental function is_executable_target(target_ptr) result(is_exe)
+ type(build_target_t), intent(in) :: target_ptr
+ logical :: is_exe
+ is_exe = target_ptr%target_type == FPM_TARGET_EXECUTABLE .and. &
+ allocated(target_ptr%dependencies)
+ if (is_exe) then
+ is_exe = target_ptr%dependencies(1)%ptr%source%unit_scope == FPM_SCOPE_APP
+ end if
+ end function is_executable_target
+
+ elemental function is_module_file(name) result(is_mod)
+ character(len=*), intent(in) :: name
+ logical :: is_mod
+ integer :: ll
+ ll = len(name)
+ is_mod = name(max(1, ll-3):ll) == ".mod"
+ end function is_module_file
+
+ subroutine handle_error(error)
+ type(error_t), intent(in), optional :: error
+ if (present(error)) then
+ print '("[Error]", 1x, a)', error%message
+ error stop 1
+ end if
+ end subroutine handle_error
+
+end module fpm_cmd_install
diff --git a/src/fpm/cmd/new.f90 b/src/fpm/cmd/new.f90
new file mode 100644
index 0000000..5149bea
--- /dev/null
+++ b/src/fpm/cmd/new.f90
@@ -0,0 +1,652 @@
+module fpm_cmd_new
+!># Definition of the "new" subcommand
+!>
+!> A type of the general command base class [[fpm_cmd_settings]]
+!> was created for the "new" subcommand ==> type [[fpm_new_settings]].
+!> This procedure read the values that were set on the command line
+!> from this type to decide what actions to take.
+!>
+!> It is virtually self-contained and so independant of the rest of the
+!> application that it could function as a separate program.
+!>
+!> The "new" subcommand options currently consist of a SINGLE top
+!> directory name to create that must have a name that is an
+!> allowable Fortran variable name. That should have been ensured
+!> by the command line processing before this procedure is called.
+!> So basically this routine has already had the options vetted and
+!> just needs to conditionally create a few files.
+!>
+!> As described in the documentation it will selectively
+!> create the subdirectories app/, test/, src/, and example/
+!> and populate them with sample files.
+!>
+!> It also needs to create an initial manifest file "fpm.toml".
+!>
+!> It then calls the system command "git init".
+!>
+!> It should test for file existence and not overwrite existing
+!> files and inform the user if there were conflicts.
+!>
+!> Any changes should be reflected in the documentation in
+!> [[fpm_command_line.f90]]
+!>
+!> FUTURE
+!> A filename like "." would need system commands or a standard routine
+!> like realpath(3c) to process properly.
+!>
+!> Perhaps allow more than one name on a single command. It is an arbitrary
+!> restriction based on a concensus preference, not a required limitation.
+!>
+!> Initially the name of the directory is used as the module name in the
+!> src file so it must be an allowable Fortran variable name. If there are
+!> complaints about it it might be changed. Handling unicode at this point
+!> might be problematic as not all current compilers handle it. Other
+!> utilities like content trackers (ie. git) or repositories like github
+!> might also have issues with alternative names or names with spaces, etc.
+!> So for the time being it seems prudent to encourage simple ASCII top directory
+!> names (similiar to the primary programming language Fortran itself).
+!>
+!> Should be able to create or pull more complicated initial examples
+!> based on various templates. It should place or mention other relevant
+!> documents such as a description of the manifest file format in user hands;
+!> or how to access registered packages and local packages,
+!> although some other command might provide that (and the help command should
+!> be the first go-to for a CLI utility).
+
+use fpm_command_line, only : fpm_new_settings
+use fpm_environment, only : run, OS_LINUX, OS_MACOS, OS_WINDOWS
+use fpm_filesystem, only : join_path, exists, basename, mkdir, is_dir, to_fortran_name
+use fpm_filesystem, only : fileopen, fileclose, filewrite, warnwrite
+use fpm_strings, only : join
+use,intrinsic :: iso_fortran_env, only : stderr=>error_unit
+implicit none
+private
+public :: cmd_new
+
+contains
+
+subroutine cmd_new(settings)
+type(fpm_new_settings), intent(in) :: settings
+integer,parameter :: tfc = selected_char_kind('DEFAULT')
+character(len=:,kind=tfc),allocatable :: bname ! baeename of NAME
+character(len=:,kind=tfc),allocatable :: tomlfile(:)
+character(len=:,kind=tfc),allocatable :: littlefile(:)
+
+ !> TOP DIRECTORY NAME PROCESSING
+ !> see if requested new directory already exists and process appropriately
+ if(exists(settings%name) .and. .not.settings%backfill )then
+ write(stderr,'(*(g0,1x))')&
+ & '<ERROR>',settings%name,'already exists.'
+ write(stderr,'(*(g0,1x))')&
+ & ' perhaps you wanted to add --backfill ?'
+ return
+ elseif(is_dir(settings%name) .and. settings%backfill )then
+ write(*,'(*(g0))')'backfilling ',settings%name
+ elseif(exists(settings%name) )then
+ write(stderr,'(*(g0,1x))')&
+ & '<ERROR>',settings%name,'already exists and is not a directory.'
+ return
+ else
+ ! make new directory
+ call mkdir(settings%name)
+ endif
+
+ !> temporarily change to new directory as a test. NB: System dependent
+ call run('cd '//settings%name)
+ ! NOTE: need some system routines to handle filenames like "."
+ ! like realpath() or getcwd().
+ bname=basename(settings%name)
+
+ ! create NAME/.gitignore file
+ call warnwrite(join_path(settings%name, '.gitignore'), ['build/*'])
+
+ littlefile=[character(len=80) :: '# '//bname, 'My cool new project!']
+
+ ! create NAME/README.md
+ call warnwrite(join_path(settings%name, 'README.md'), littlefile)
+
+ ! start building NAME/fpm.toml
+ if(settings%with_full)then
+ tomlfile=[character(len=80) :: &
+ &' # This is your fpm(Fortran Package Manager) manifest file ',&
+ &' # ("fpm.toml"). It is heavily annotated to help guide you though ',&
+ &' # customizing a package build, although the defaults are sufficient ',&
+ &' # for many basic packages. ',&
+ &' # ',&
+ &' # The manifest file is not only used to provide metadata identifying ',&
+ &' # your project (so it can be used by others as a dependency). It can ',&
+ &' # specify where your library and program sources live, what the name ',&
+ &' # of the executable(s) will be, what files to build, dependencies on ',&
+ &' # other fpm packages, and what external libraries are required. ',&
+ &' # ',&
+ &' # The manifest format must conform to the TOML configuration file ',&
+ &' # standard. ',&
+ &' # ',&
+ &' # TOML files support flexible use of white-space and commenting of the ',&
+ &' # configuration data, but for clarity in this sample active directives ',&
+ &' # begin in column one. Inactive example directives are commented ',&
+ &' # out with a pound character ("#") but begin in column one as well. ',&
+ &' # Commentary begins with a pound character in column three. ',&
+ &' # ',&
+ &' # This file draws heavily upon the following references: ',&
+ &' # ',&
+ &' # The fpm home page at ',&
+ &' # https://github.com/fortran-lang/fpm ',&
+ &' # A complete list of keys and their attributes at ',&
+ &' # https://github.com/fortran-lang/fpm/blob/master/manifest-reference.md ',&
+ &' # examples of fpm project packaging at ',&
+ &' # https://github.com/fortran-lang/fpm/blob/master/PACKAGING.md ',&
+ &' # The Fortran TOML file interface and it''s references at ',&
+ &' # https://github.com/toml-f/toml-f ',&
+ &' # ',&
+ &' #----------------------- ',&
+ &' # project Identification ',&
+ &' #----------------------- ',&
+ &' # We begin with project metadata at the manifest root. This data is designed ',&
+ &' # to aid others when searching for the project in a repository and to ',&
+ &' # identify how and when to contact the package supporters. ',&
+ &' ',&
+ &'name = "'//bname//'"',&
+ &' # The project name (required) is how the project will be referred to. ',&
+ &' # The name is used by other packages using it as a dependency. It also ',&
+ &' # is used as the default name of any library built and the optional ',&
+ &' # default executable built from app/main.f90. It must conform to the rules ',&
+ &' # for a Fortran variable name. ',&
+ &' ',&
+ &'version = "0.1.0" ',&
+ &' # The project version number is a string. A recommended scheme for ',&
+ &' # specifying versions is the Semantic Versioning scheme. ',&
+ &' ',&
+ &'license = "license" ',&
+ &' # Licensing information specified using SPDX identifiers is preferred ',&
+ &' # (eg. "Apache-2.0 OR MIT" or "LGPL-3.0-or-later"). ',&
+ &' ',&
+ &'maintainer = "jane.doe@example.com" ',&
+ &' # Information on the project maintainer and means to reach out to them. ',&
+ &' ',&
+ &'author = "Jane Doe" ',&
+ &' # Information on the project author. ',&
+ &' ',&
+ &'copyright = "Copyright 2020 Jane Doe" ',&
+ &' # A statement clarifying the Copyright status of the project. ',&
+ &' ',&
+ &'#description = "A short project summary in plain text" ',&
+ &' # The description provides a short summary on the project. It should be ',&
+ &' # plain text and not use any markup formatting. ',&
+ &' ',&
+ &'#categories = ["fortran", "graphics"] ',&
+ &' # Categories associated with the project. Listing only one is preferred. ',&
+ &' ',&
+ &'#keywords = ["hdf5", "mpi"] ',&
+ &' # The keywords field is an array of strings describing the project. ',&
+ &' ',&
+ &'#homepage = "https://stdlib.fortran-lang.org" ',&
+ &' # URL to the webpage of the project. ',&
+ &' ',&
+ &' # ----------------------------------------- ',&
+ &' # We are done with identifying the project. ',&
+ &' # ----------------------------------------- ',&
+ &' # ',&
+ &' # Now lets start describing how the project should be built. ',&
+ &' # ',&
+ &' # Note tables would go here but we will not be talking about them (much)!!' ,&
+ &' # ',&
+ &' # Tables are a way to explicitly specify large numbers of programs in ',&
+ &' # a compact format instead of individual per-program entries in the ',&
+ &' # [[executable]], [[test]], and [[example]] sections to follow but ',&
+ &' # will not be discussed further except for the following notes: ',&
+ &' # ',&
+ &' # + Tables must appear (here) before any sections are declared. Once a ',&
+ &' # section is specified in a TOML file everything afterwards must be ',&
+ &' # values for that section or the beginning of a new section. A simple ',&
+ &' # example looks like: ',&
+ &' ',&
+ &'#executable = [ ',&
+ &'# { name = "a-prog" }, ',&
+ &'# { name = "app-tool", source-dir = "tool" }, ',&
+ &'# { name = "fpm-man", source-dir = "tool", main="fman.f90" } ',&
+ &'#] ',&
+ &' ',&
+ &' # This would be in lieue of the [[executable]] section found later in this ',&
+ &' # configuration file. ',&
+ &' # + See the reference documents (at the beginning of this document) ',&
+ &' # for more information on tables if you have long lists of programs ',&
+ &' # to build and are not simply depending on auto-detection. ',&
+ &' # ',&
+ &' # Now lets begin the TOML sections (lines beginning with "[") ... ',&
+ &' # ',&
+ &' ',&
+ &'[install] # Options for the "install" subcommand ',&
+ &' ',&
+ &' # When you run the "install" subcommand only executables are installed by ',&
+ &' # default on the local system. Library projects that will be used outside of ',&
+ &' # "fpm" can set the "library" boolean to also allow installing the module ',&
+ &' # files and library archive. Without this being set to "true" an "install" ',&
+ &' # subcommand ignores parameters that specify library installation. ',&
+ &' ',&
+ &'library = false ',&
+ &' ',&
+ &'[build] # General Build Options ',&
+ &' ',&
+ &' ### Automatic target discovery ',&
+ &' # ',&
+ &' # Normally fpm recursively searches the app/, example/, and test/ directories ',&
+ &' # for program sources and builds them. To disable this automatic discovery of ',&
+ &' # program targets set the following to "false": ',&
+ &' ',&
+ &'#auto-executables = true ',&
+ &'#auto-examples = true ',&
+ &'#auto-tests = true ',&
+ &' ',&
+ &' ### Package-level External Library Links ',&
+ &' # ',&
+ &' # To declare link-time dependencies on external libraries a list of ',&
+ &' # native libraries can be specified with the "link" entry. You may ',&
+ &' # have one library name or a list of strings in case several ',&
+ &' # libraries should be linked. This list of library dependencies is ',&
+ &' # exported to dependent packages. You may have to alter your library ',&
+ &' # search-path to ensure the libraries can be accessed. Typically, ',&
+ &' # this is done with the LD_LIBRARY_PATH environment variable on ULS ',&
+ &' # (Unix-Like Systems). You only specify the core name of the library ',&
+ &' # (as is typical with most programming environments, where you ',&
+ &' # would specify "-lz" on your load command to link against the zlib ',&
+ &' # compression library even though the library file would typically be ',&
+ &' # a file called "libz.a" "or libz.so"). So to link against that library ',&
+ &' # you would specify: ',&
+ &' ',&
+ &'#link = "z" ',&
+ &' ',&
+ &' # Note that in some cases the order of the libraries matters: ',&
+ &' ',&
+ &'#link = ["blas", "lapack"] ',&
+ &'']
+ endif
+
+ if(settings%with_bare)then
+ elseif(settings%with_lib)then
+ call mkdir(join_path(settings%name,'src') )
+ ! create next section of fpm.toml
+ if(settings%with_full)then
+ tomlfile=[character(len=80) :: tomlfile, &
+ &'[library] ',&
+ &' ',&
+ &' # You can change the name of the directory to search for your library ',&
+ &' # source from the default of "src/". Library targets are exported ',&
+ &' # and usable by other projects. ',&
+ &' ',&
+ &'source-dir="src" ',&
+ &' ',&
+ &' # this can be a list: ',&
+ &' ',&
+ &'#source-dir=["src", "src2"] ',&
+ &' ',&
+ &' # More complex libraries may organize their modules in subdirectories. ',&
+ &' # For modules in a top-level directory fpm requires (but does not ',&
+ &' # enforce) that: ',&
+ &' # ',&
+ &' # + The module has the same name as the source file. This is important. ',&
+ &' # + There should be only one module per file. ',&
+ &' # ',&
+ &' # These two requirements simplify the build process for fpm. As Fortran ',&
+ &' # compilers emit module files (.mod) with the same name as the module ',&
+ &' # itself (but not the source file, .f90), naming the module the same ',&
+ &' # as the source file allows fpm to: ',&
+ &' # ',&
+ &' # + Uniquely and exactly map a source file (.f90) to its object (.o) ',&
+ &' # and module (.mod) files. ',&
+ &' # + Avoid conflicts with modules of the same name that could appear ',&
+ &' # in dependency packages. ',&
+ &' # ',&
+ &' ### Multi-level library source ',&
+ &' # You can place your module source files in any number of levels of ',&
+ &' # subdirectories inside your source directory, but there are certain naming ',&
+ &' # conventions to be followed -- module names must contain the path components ',&
+ &' # of the directory that its source file is in. ',&
+ &' # ',&
+ &' # This rule applies generally to any number of nested directories and ',&
+ &' # modules. For example, src/a/b/c/d.f90 must define a module called a_b_c_d. ',&
+ &' # Again, this is not enforced but may be required in future releases. ',&
+ &'']
+ endif
+ ! create placeholder module src/bname.f90
+ littlefile=[character(len=80) :: &
+ &'module '//to_fortran_name(bname), &
+ &' implicit none', &
+ &' private', &
+ &'', &
+ &' public :: say_hello', &
+ &'contains', &
+ &' subroutine say_hello', &
+ &' print *, "Hello, '//bname//'!"', &
+ &' end subroutine say_hello', &
+ &'end module '//to_fortran_name(bname)]
+ ! create NAME/src/NAME.f90
+ call warnwrite(join_path(settings%name, 'src', bname//'.f90'),&
+ & littlefile)
+ endif
+
+ if(settings%with_full)then
+ tomlfile=[character(len=80) :: tomlfile ,&
+ &'[dependencies] ',&
+ &' ',&
+ &' # Inevitably, you will want to be able to include other packages in ',&
+ &' # a project. Fpm makes this incredibly simple, by taking care of ',&
+ &' # fetching and compiling your dependencies for you. You just tell it ',&
+ &' # what your dependencies names are, and where to find them. ',&
+ &' # ',&
+ &' # If you are going to distribute your package only place dependencies ',&
+ &' # here someone using your package as a remote dependency needs built. ',&
+ &' # You can define dependencies just for developer executables in the ',&
+ &' # next section, or even for specific executables as we will see below ',&
+ &' # (Then fpm will still fetch and compile it when building your ',&
+ &' # developer executables, but users of your library will not have to). ',&
+ &' # ',&
+ &' ## GLOBAL DEPENDENCIES (exported with your project) ',&
+ &' # ',&
+ &' # Typically, dependencies are defined by specifying the project''s ',&
+ &' # git repository. ',&
+ &' # ',&
+ &' # You can be specific about which version of a dependency you would ',&
+ &' # like. By default the latest master master branch is used. You can ',&
+ &' # optionally specify a branch, a tag or a commit value. ',&
+ &' # ',&
+ &' # So here are several alternates for specifying a remote dependency (you ',&
+ &' # can have at most one of "branch", "rev" or "tag" present): ',&
+ &' ',&
+ &'#stdlib = { git = "https://github.com/LKedward/stdlib-fpm.git" } ',&
+ &'#stdlib = {git="https://github.com/LKedward/stdlib-fpm.git",branch = "master" },',&
+ &'#stdlib = {git="https://github.com/LKedward/stdlib-fpm.git", tag = "v0.1.0" }, ',&
+ &'#stdlib = {git="https://github.com/LKedward/stdlib-fpm.git", rev = "5a9b7a8" }. ',&
+ &' ',&
+ &' # There may be multiple packages listed: ',&
+ &' ',&
+ &'#M_strings = { git = "https://github.com/urbanjost/M_strings.git" } ',&
+ &'#M_time = { git = "https://github.com/urbanjost/M_time.git" } ',&
+ &' ',&
+ &' # ',&
+ &' # You can even specify the local path to another project if it is in ',&
+ &' # a sub-folder (If for example you have got another fpm package **in ',&
+ &' # the same repository**) like this: ',&
+ &' ',&
+ &'#M_strings = { path = "M_strings" } ',&
+ &' ',&
+ &' # If you specify paths outside of your repository (ie. paths with a ',&
+ &' # slash in them) things will not work for your users! ',&
+ &' # ',&
+ &' # For a more verbose layout use normal tables rather than inline tables ',&
+ &' # to specify dependencies: ',&
+ &' ',&
+ &'#[dependencies.toml-f] ',&
+ &'#git = "https://github.com/toml-f/toml-f" ',&
+ &'#rev = "2f5eaba864ff630ba0c3791126a3f811b6e437f3" ',&
+ &' ',&
+ &' # Now you can use any modules from these libraries anywhere in your ',&
+ &' # code -- whether is in your library source or a program source. ',&
+ &' ',&
+ &'[dev-dependencies] ',&
+ &' ',&
+ &' ## Dependencies Only for Development ',&
+ &' # ',&
+ &' # You can specify dependencies your library or application does not ',&
+ &' # depend on in a similar way. The difference is that these will not ',&
+ &' # be exported as part of your project to those using it as a remote ',&
+ &' # dependency. ',&
+ &' # ',&
+ &' # Currently, like a global dependency it will still be available for ',&
+ &' # all codes. It is up to the developer to ensure that nothing except ',&
+ &' # developer test programs rely upon it. ',&
+ &' ',&
+ &'#M_msg = { git = "https://github.com/urbanjost/M_msg.git" } ',&
+ &'#M_verify = { git = "https://github.com/urbanjost/M_verify.git" } ',&
+ &'']
+ endif
+ if(settings%with_bare)then
+ elseif(settings%with_executable)then
+ ! create next section of fpm.toml
+ call mkdir(join_path(settings%name, 'app'))
+ ! create NAME/app or stop
+ if(settings%with_full)then
+ tomlfile=[character(len=80) :: tomlfile, &
+ &' #----------------------------------- ',&
+ &' ## Application-specific declarations ',&
+ &' #----------------------------------- ',&
+ &' # Now lets begin entries for the TOML tables (lines beginning with "[[") ',&
+ &' # that describe the program sources -- applications, tests, and examples. ',&
+ &' # ',&
+ &' # First we will configuration individual applications run with "fpm run". ',&
+ &' # ',&
+ &' # + the "name" entry for the executable to be built must always ',&
+ &' # be specified. The name must satisfy the rules for a Fortran ',&
+ &' # variable name. This will be the name of the binary installed by ',&
+ &' # the "install" subcommand and used on the "run" subcommand. ',&
+ &' # + The source directory for each executable can be adjusted by the ',&
+ &' # "source-dir" entry. ',&
+ &' # + The basename of the source file containing the program body can ',&
+ &' # be specified with the "main" entry. ',&
+ &' # + Executables can also specify their own external package and ',&
+ &' # library link dependencies. ',&
+ &' # ',&
+ &' # Currently, like a global dependency any external package dependency ',&
+ &' # will be available for all codes. It is up to the developer to ensure ',&
+ &' # that nothing except the application programs specified rely upon it. ',&
+ &' # ',&
+ &' # Note if your application needs to use a module internally, but you do not ',&
+ &' # intend to build it as a library to be used in other projects, you can ',&
+ &' # include the module in your program source file or directory as well. ',&
+ &' ',&
+ &'[[executable]] ',&
+ &'name="'//bname//'"',&
+ &'source-dir="app" ',&
+ &'main="main.f90" ',&
+ &' ',&
+ &' # You may repeat this pattern to define additional applications. For instance,',&
+ &' # the following sample illustrates all accepted options, where "link" and ',&
+ &' # "executable.dependencies" keys are the same as the global external library ',&
+ &' # links and package dependencies described previously except they apply ',&
+ &' # only to this executable: ',&
+ &' ',&
+ &'#[[ executable ]] ',&
+ &'#name = "app-name" ',&
+ &'#source-dir = "prog" ',&
+ &'#main = "program.f90" ',&
+ &'#link = "z" ',&
+ &'#[executable.dependencies] ',&
+ &'#M_CLI = { git = "https://github.com/urbanjost/M_CLI.git" } ',&
+ &'#helloff = { git = "https://gitlab.com/everythingfunctional/helloff.git" } ',&
+ &'#M_path = { git = "https://github.com/urbanjost/M_path.git" } ',&
+ &'']
+ endif
+
+ if(exists(bname//'/src/'))then
+ littlefile=[character(len=80) :: &
+ &'program main', &
+ &' use '//to_fortran_name(bname)//', only: say_hello', &
+ &' implicit none', &
+ &'', &
+ &' call say_hello()', &
+ &'end program main']
+ else
+ littlefile=[character(len=80) :: &
+ &'program main', &
+ &' implicit none', &
+ &'', &
+ &' print *, "hello from project '//bname//'"', &
+ &'end program main']
+ endif
+ call warnwrite(join_path(settings%name, 'app/main.f90'), littlefile)
+ endif
+
+ if(settings%with_bare)then
+ elseif(settings%with_test)then
+
+ ! create NAME/test or stop
+ call mkdir(join_path(settings%name, 'test'))
+ ! create next section of fpm.toml
+ if(settings%with_full)then
+ tomlfile=[character(len=80) :: tomlfile ,&
+ &'[[test]] ',&
+ &' ',&
+ &' # The same declarations can be made for test programs, which are ',&
+ &' # executed with the "fpm test" command and are not build when your ',&
+ &' # package is used as a dependency by other packages. These are ',&
+ &' # typically unit tests of the package only used during package ',&
+ &' # development. ',&
+ &' ',&
+ &'name="runTests" ',&
+ &'source-dir="test" ',&
+ &'main="check.f90" ',&
+ &' ',&
+ &' # you may repeat this pattern to add additional explicit test program ',&
+ &' # parameters. The following example contains a sample of all accepted ',&
+ &' # options. ',&
+ &' ',&
+ &'#[[ test ]] ',&
+ &'#name = "tester" ',&
+ &'#source-dir="test" ',&
+ &'#main="tester.f90" ',&
+ &'#link = ["blas", "lapack"] ',&
+ &'#[test.dependencies] ',&
+ &'#M_CLI2 = { git = "https://github.com/urbanjost/M_CLI2.git" } ',&
+ &'#M_io = { git = "https://github.com/urbanjost/M_io.git" } ',&
+ &'#M_system= { git = "https://github.com/urbanjost/M_system.git" } ',&
+ &'']
+ endif
+
+ littlefile=[character(len=80) :: &
+ &'program check', &
+ &'implicit none', &
+ &'', &
+ &'print *, "Put some tests in here!"', &
+ &'end program check']
+ ! create NAME/test/check.f90
+ call warnwrite(join_path(settings%name, 'test/check.f90'), littlefile)
+ endif
+
+ if(settings%with_bare)then
+ elseif(settings%with_example)then
+
+ ! create NAME/example or stop
+ call mkdir(join_path(settings%name, 'example'))
+ ! create next section of fpm.toml
+ if(settings%with_full)then
+ tomlfile=[character(len=80) :: tomlfile, &
+ &'[[example]] ',&
+ &' ',&
+ &' # Example applications for a project are defined here. ',&
+ &' # These are run via "fpm run --example NAME" and like the ',&
+ &' # test applications, are not built when this package is used as a ',&
+ &' # dependency by other packages. ',&
+ &' ',&
+ &'name="demo" ',&
+ &'source-dir="example" ',&
+ &'main="demo.f90" ',&
+ &' ',&
+ &' # ',&
+ &' # you may add additional programs to the example table. The following ',&
+ &' # example contains a sample of all accepted options ',&
+ &' ',&
+ &'#[[ example ]] ',&
+ &'#name = "example-tool" ',&
+ &'#source-dir="example" ',&
+ &'#main="tool.f90" ',&
+ &'#link = "z" ',&
+ &'#[example.dependencies] ',&
+ &'#M_kracken95 = { git = "https://github.com/urbanjost/M_kracken95.git" } ',&
+ &'#datetime = {git = "https://github.com/wavebitscientific/datetime-fortran.git" }',&
+ &'']
+ endif
+
+ littlefile=[character(len=80) :: &
+ &'program demo', &
+ &'implicit none', &
+ &'', &
+ &'print *, "Put some examples in here!"', &
+ &'end program demo']
+ ! create NAME/example/demo.f90
+ call warnwrite(join_path(settings%name, 'example/demo.f90'), littlefile)
+ endif
+
+ ! now that built it write NAME/fpm.toml
+ if( allocated(tomlfile) )then
+ call validate_toml_data(tomlfile)
+ call warnwrite(join_path(settings%name, 'fpm.toml'), tomlfile)
+ else
+ call create_verified_basic_manifest(join_path(settings%name, 'fpm.toml'))
+ endif
+ ! assumes git(1) is installed and in path
+ call run('git init ' // settings%name)
+contains
+
+subroutine create_verified_basic_manifest(filename)
+!> create a basic but verified default manifest file
+use fpm_toml, only : toml_table, toml_serializer, set_value
+use fpm_manifest_package, only : package_config_t, new_package
+use fpm_error, only : error_t
+implicit none
+character(len=*),intent(in) :: filename
+ type(toml_table) :: table
+ type(toml_serializer) :: ser
+ type(package_config_t) :: package
+ type(error_t), allocatable :: error
+ integer :: lun
+ character(len=8) :: date
+
+ !> get date to put into metadata in manifest file "fpm.toml"
+ call date_and_time(DATE=date)
+ table = toml_table()
+ ser = toml_serializer()
+ call fileopen(filename,lun) ! fileopen stops on error
+
+ call set_value(table, "name", BNAME)
+ call set_value(table, "version", "0.1.0")
+ call set_value(table, "license", "license")
+ call set_value(table, "author", "Jane Doe")
+ call set_value(table, "maintainer", "jane.doe@example.com")
+ call set_value(table, "copyright", 'Copyright '//date(1:4)//', Jane Doe')
+ ! continue building of manifest
+ ! ...
+ call new_package(package, table, error)
+ if (allocated(error)) stop 3
+ if(settings%verbose)then
+ call table%accept(ser)
+ endif
+ ser%unit=lun
+ call table%accept(ser)
+ call fileclose(lun) ! fileopen stops on error
+
+end subroutine create_verified_basic_manifest
+
+
+subroutine validate_toml_data(input)
+!> verify a string array is a valid fpm.toml file
+!
+use tomlf, only : toml_parse
+use fpm_toml, only : toml_table, toml_serializer
+implicit none
+character(kind=tfc,len=:),intent(in),allocatable :: input(:)
+character(len=1), parameter :: nl = new_line('a')
+type(toml_table), allocatable :: table
+character(kind=tfc, len=:), allocatable :: joined_string
+type(toml_serializer) :: ser
+
+! you have to add a newline character by using the intrinsic
+! function `new_line("a")` to get the lines processed correctly.
+joined_string = join(input,right=nl)
+
+if (allocated(table)) deallocate(table)
+call toml_parse(table, joined_string)
+if (allocated(table)) then
+ if(settings%verbose)then
+ ! If the TOML file is successfully parsed the table will be allocated and
+ ! can be written to the standard output by passing the `toml_serializer`
+ ! as visitor to the table.
+ call table%accept(ser)
+ endif
+ call table%destroy
+endif
+
+end subroutine validate_toml_data
+
+end subroutine cmd_new
+
+end module fpm_cmd_new
diff --git a/src/fpm/cmd/update.f90 b/src/fpm/cmd/update.f90
new file mode 100644
index 0000000..d7cc549
--- /dev/null
+++ b/src/fpm/cmd/update.f90
@@ -0,0 +1,68 @@
+module fpm_cmd_update
+ use fpm_command_line, only : fpm_update_settings
+ use fpm_dependency, only : dependency_tree_t, new_dependency_tree
+ use fpm_error, only : error_t
+ use fpm_filesystem, only : exists, mkdir, join_path, delete_file
+ use fpm_manifest, only : package_config_t, get_package_data
+ implicit none
+ private
+ public :: cmd_update
+
+contains
+
+ !> Entry point for the update subcommand
+ subroutine cmd_update(settings)
+ !> Representation of the command line arguments
+ type(fpm_update_settings), intent(in) :: settings
+ type(package_config_t) :: package
+ type(dependency_tree_t) :: deps
+ type(error_t), allocatable :: error
+
+ integer :: ii
+ character(len=:), allocatable :: cache
+
+ call get_package_data(package, "fpm.toml", error, apply_defaults=.true.)
+ call handle_error(error)
+
+ if (.not.exists("build")) then
+ call mkdir("build")
+ end if
+
+ cache = join_path("build", "cache.toml")
+ if (settings%clean) then
+ call delete_file(cache)
+ end if
+
+ call new_dependency_tree(deps, cache=cache, &
+ verbosity=merge(2, 1, settings%verbose))
+
+ call deps%add(package, error)
+ call handle_error(error)
+
+ if (settings%fetch_only) return
+
+ if (size(settings%name) == 0) then
+ do ii = 1, deps%ndep
+ call deps%update(deps%dep(ii)%name, error)
+ call handle_error(error)
+ end do
+ else
+ do ii = 1, size(settings%name)
+ call deps%update(trim(settings%name(ii)), error)
+ call handle_error(error)
+ end do
+ end if
+
+ end subroutine cmd_update
+
+ !> Error handling for this command
+ subroutine handle_error(error)
+ !> Potential error
+ type(error_t), intent(in), optional :: error
+ if (present(error)) then
+ print '(a)', error%message
+ error stop 1
+ end if
+ end subroutine handle_error
+
+end module fpm_cmd_update
diff --git a/src/fpm/dependency.f90 b/src/fpm/dependency.f90
new file mode 100644
index 0000000..144ffbe
--- /dev/null
+++ b/src/fpm/dependency.f90
@@ -0,0 +1,821 @@
+!> # Dependency management
+!>
+!> ## Fetching dependencies and creating a dependency tree
+!>
+!> Dependencies on the top-level can be specified from:
+!>
+!> - `package%dependencies`
+!> - `package%dev_dependencies`
+!> - `package%executable(:)%dependencies`
+!> - `package%test(:)%dependencies`
+!>
+!> Each dependency is fetched in some way and provides a path to its package
+!> manifest.
+!> The `package%dependencies` of the dependencies are resolved recursively.
+!>
+!> To initialize the dependency tree all dependencies are recursively fetched
+!> and stored in a flat data structure to avoid retrieving a package twice.
+!> The data structure used to store this information should describe the current
+!> status of the dependency tree. Important information are:
+!>
+!> - name of the package
+!> - version of the package
+!> - path to the package root
+!>
+!> Additionally, for version controlled dependencies the following should be
+!> stored along with the package:
+!>
+!> - the upstream url
+!> - the current checked out revision
+!>
+!> Fetching a remote (version controlled) dependency turns it for our purpose
+!> into a local path dependency which is handled by the same means.
+!>
+!> ## Updating dependencies
+!>
+!> For a given dependency tree all top-level dependencies can be updated.
+!> We have two cases to consider, a remote dependency and a local dependency,
+!> again, remote dependencies turn into local dependencies by fetching.
+!> Therefore we will update remote dependencies by simply refetching them.
+!>
+!> For remote dependencies we have to refetch if the revision in the manifest
+!> changes or the upstream HEAD has changed (for branches _and_ tags).
+!>
+!> @Note For our purpose a tag is just a fancy branch name. Tags can be delete and
+!> modified afterwards, therefore they do not differ too much from branches
+!> from our perspective.
+!>
+!> For the latter case we only know if we actually fetch from the upstream URL.
+!>
+!> In case of local (and fetched remote) dependencies we have to read the package
+!> manifest and compare its dependencies against our dependency tree, any change
+!> requires updating the respective dependencies as well.
+!>
+!> ## Handling dependency compatibilties
+!>
+!> Currenly ignored. First come, first serve.
+module fpm_dependency
+ use, intrinsic :: iso_fortran_env, only : output_unit
+ use fpm_environment, only : get_os_type, OS_WINDOWS
+ use fpm_error, only : error_t, fatal_error
+ use fpm_filesystem, only : exists, join_path, mkdir, canon_path, windows_path
+ use fpm_git, only : git_target_revision, git_target_default, git_revision
+ use fpm_manifest, only : package_config_t, dependency_config_t, &
+ get_package_data
+ use fpm_strings, only : string_t, operator(.in.)
+ use fpm_toml, only : toml_table, toml_key, toml_error, toml_serializer, &
+ toml_parse, get_value, set_value, add_table
+ use fpm_versioning, only : version_t, new_version, char
+ implicit none
+ private
+
+ public :: dependency_tree_t, new_dependency_tree
+ public :: dependency_node_t, new_dependency_node
+ public :: resize
+
+
+ !> Overloaded reallocation interface
+ interface resize
+ module procedure :: resize_dependency_node
+ end interface resize
+
+
+ !> Dependency node in the projects dependency tree
+ type, extends(dependency_config_t) :: dependency_node_t
+ !> Actual version of this dependency
+ type(version_t), allocatable :: version
+ !> Installation prefix of this dependencies
+ character(len=:), allocatable :: proj_dir
+ !> Checked out revision of the version control system
+ character(len=:), allocatable :: revision
+ !> Dependency is handled
+ logical :: done = .false.
+ !> Dependency should be updated
+ logical :: update = .false.
+ contains
+ !> Update dependency from project manifest
+ procedure :: register
+ end type dependency_node_t
+
+
+ !> Respresentation of a projects dependencies
+ !>
+ !> The dependencies are stored in a simple array for now, this can be replaced
+ !> with a binary-search tree or a hash table in the future.
+ type :: dependency_tree_t
+ !> Unit for IO
+ integer :: unit = output_unit
+ !> Verbosity of printout
+ integer :: verbosity = 1
+ !> Installation prefix for dependencies
+ character(len=:), allocatable :: dep_dir
+ !> Number of currently registered dependencies
+ integer :: ndep = 0
+ !> Flattend list of all dependencies
+ type(dependency_node_t), allocatable :: dep(:)
+ !> Cache file
+ character(len=:), allocatable :: cache
+ contains
+ !> Overload procedure to add new dependencies to the tree
+ generic :: add => add_project, add_project_dependencies, add_dependencies, &
+ add_dependency
+ !> Main entry point to add a project
+ procedure, private :: add_project
+ !> Add a project and its dependencies to the dependency tree
+ procedure, private :: add_project_dependencies
+ !> Add a list of dependencies to the dependency tree
+ procedure, private :: add_dependencies
+ !> Add a single dependency to the dependency tree
+ procedure, private :: add_dependency
+ !> Resolve dependencies
+ generic :: resolve => resolve_dependencies, resolve_dependency
+ !> Resolve dependencies
+ procedure, private :: resolve_dependencies
+ !> Resolve dependencies
+ procedure, private :: resolve_dependency
+ !> Find a dependency in the tree
+ generic :: find => find_dependency, find_name
+ !> Find a dependency from an dependency configuration
+ procedure, private :: find_dependency
+ !> Find a dependency by its name
+ procedure, private :: find_name
+ !> Depedendncy resolution finished
+ procedure :: finished
+ !> Reading of dependency tree
+ generic :: load => load_from_file, load_from_unit, load_from_toml
+ !> Read dependency tree from file
+ procedure, private :: load_from_file
+ !> Read dependency tree from formatted unit
+ procedure, private :: load_from_unit
+ !> Read dependency tree from TOML data structure
+ procedure, private :: load_from_toml
+ !> Writing of dependency tree
+ generic :: dump => dump_to_file, dump_to_unit, dump_to_toml
+ !> Write dependency tree to file
+ procedure, private :: dump_to_file
+ !> Write dependency tree to formatted unit
+ procedure, private :: dump_to_unit
+ !> Write dependency tree to TOML data structure
+ procedure, private :: dump_to_toml
+ !> Update dependency tree
+ generic :: update => update_dependency
+ !> Update a list of dependencies
+ procedure, private :: update_dependency
+ end type dependency_tree_t
+
+ !> Common output format for writing to the command line
+ character(len=*), parameter :: out_fmt = '("#", *(1x, g0))'
+
+contains
+
+ !> Create a new dependency tree
+ subroutine new_dependency_tree(self, verbosity, cache)
+ !> Instance of the dependency tree
+ type(dependency_tree_t), intent(out) :: self
+ !> Verbosity of printout
+ integer, intent(in), optional :: verbosity
+ !> Name of the cache file
+ character(len=*), intent(in), optional :: cache
+
+ call resize(self%dep)
+ self%dep_dir = join_path("build", "dependencies")
+
+ if (present(verbosity)) then
+ self%verbosity = verbosity
+ end if
+
+ if (present(cache)) then
+ self%cache = cache
+ end if
+
+ end subroutine new_dependency_tree
+
+ !> Create a new dependency node from a configuration
+ pure subroutine new_dependency_node(self, dependency, version, proj_dir, update)
+ !> Instance of the dependency node
+ type(dependency_node_t), intent(out) :: self
+ !> Dependency configuration data
+ type(dependency_config_t), intent(in) :: dependency
+ !> Version of the dependency
+ type(version_t), intent(in), optional :: version
+ !> Installation prefix of the dependency
+ character(len=*), intent(in), optional :: proj_dir
+ !> Dependency should be updated
+ logical, intent(in), optional :: update
+
+ self%dependency_config_t = dependency
+
+ if (present(version)) then
+ self%version = version
+ end if
+
+ if (present(proj_dir)) then
+ self%proj_dir = proj_dir
+ end if
+
+ if (present(update)) then
+ self%update = update
+ end if
+
+ end subroutine new_dependency_node
+
+ !> Add project dependencies, each depth level after each other.
+ !>
+ !> We implement this algorithm in an interative rather than a recursive fashion
+ !> as a choice of design.
+ subroutine add_project(self, package, error)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(inout) :: self
+ !> Project configuration to add
+ type(package_config_t), intent(in) :: package
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(dependency_config_t) :: dependency
+ character(len=:), allocatable :: root
+ logical :: main
+
+ if (allocated(self%cache)) then
+ call self%load(self%cache, error)
+ if (allocated(error)) return
+ end if
+
+ if (.not.exists(self%dep_dir)) then
+ call mkdir(self%dep_dir)
+ end if
+
+ root = "."
+
+ ! Create this project as the first dependency node (depth 0)
+ dependency%name = package%name
+ dependency%path = root
+ call self%add(dependency, error)
+ if (allocated(error)) return
+
+ ! Resolve the root project
+ call self%resolve(root, error)
+ if (allocated(error)) return
+
+ ! Add the root project dependencies (depth 1)
+ call self%add(package, root, .true., error)
+ if (allocated(error)) return
+
+ ! Now decent into the dependency tree, level for level
+ do while(.not.self%finished())
+ call self%resolve(root, error)
+ if (allocated(error)) exit
+ end do
+ if (allocated(error)) return
+
+ if (allocated(self%cache)) then
+ call self%dump(self%cache, error)
+ if (allocated(error)) return
+ end if
+
+ end subroutine add_project
+
+ !> Add a project and its dependencies to the dependency tree
+ recursive subroutine add_project_dependencies(self, package, root, main, error)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(inout) :: self
+ !> Project configuration to add
+ type(package_config_t), intent(in) :: package
+ !> Current project root directory
+ character(len=*), intent(in) :: root
+ !> Is the main project
+ logical, intent(in) :: main
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: ii
+
+ if (allocated(package%dependency)) then
+ call self%add(package%dependency, error)
+ if (allocated(error)) return
+ end if
+
+ if (main) then
+ if (allocated(package%dev_dependency)) then
+ call self%add(package%dev_dependency, error)
+ if (allocated(error)) return
+ end if
+
+ if (allocated(package%executable)) then
+ do ii = 1, size(package%executable)
+ if (allocated(package%executable(ii)%dependency)) then
+ call self%add(package%executable(ii)%dependency, error)
+ if (allocated(error)) exit
+ end if
+ end do
+ if (allocated(error)) return
+ end if
+
+ if (allocated(package%example)) then
+ do ii = 1, size(package%example)
+ if (allocated(package%example(ii)%dependency)) then
+ call self%add(package%example(ii)%dependency, error)
+ if (allocated(error)) exit
+ end if
+ end do
+ if (allocated(error)) return
+ end if
+
+ if (allocated(package%test)) then
+ do ii = 1, size(package%test)
+ if (allocated(package%test(ii)%dependency)) then
+ call self%add(package%test(ii)%dependency, error)
+ if (allocated(error)) exit
+ end if
+ end do
+ if (allocated(error)) return
+ end if
+ end if
+
+ end subroutine add_project_dependencies
+
+ !> Add a list of dependencies to the dependency tree
+ subroutine add_dependencies(self, dependency, error)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(inout) :: self
+ !> Dependency configuration to add
+ type(dependency_config_t), intent(in) :: dependency(:)
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: ii, ndep
+
+ ndep = size(self%dep)
+ if (ndep < size(dependency) + self%ndep) then
+ call resize(self%dep, ndep + ndep/2 + size(dependency))
+ end if
+
+ do ii = 1, size(dependency)
+ call self%add(dependency(ii), error)
+ if (allocated(error)) exit
+ end do
+ if (allocated(error)) return
+
+ end subroutine add_dependencies
+
+ !> Add a single dependency to the dependency tree
+ pure subroutine add_dependency(self, dependency, error)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(inout) :: self
+ !> Dependency configuration to add
+ type(dependency_config_t), intent(in) :: dependency
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: id
+
+ id = self%find(dependency)
+ if (id == 0) then
+ self%ndep = self%ndep + 1
+ call new_dependency_node(self%dep(self%ndep), dependency)
+ end if
+
+ end subroutine add_dependency
+
+ !> Update dependency tree
+ subroutine update_dependency(self, name, error)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(inout) :: self
+ !> Name of the dependency to update
+ character(len=*), intent(in) :: name
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: id
+ type(package_config_t) :: package
+ character(len=:), allocatable :: manifest, proj_dir, revision, root
+
+ id = self%find(name)
+ root = "."
+
+ if (id <= 0) then
+ call fatal_error(error, "Cannot update dependency '"//name//"'")
+ return
+ end if
+
+ associate(dep => self%dep(id))
+ if (allocated(dep%git) .and. dep%update) then
+ if (self%verbosity > 1) then
+ write(self%unit, out_fmt) "Update:", dep%name
+ end if
+ proj_dir = join_path(self%dep_dir, dep%name)
+ call dep%git%checkout(proj_dir, error)
+ if (allocated(error)) return
+
+ ! Unset dependency and remove updatable attribute
+ dep%done = .false.
+ dep%update = .false.
+
+ ! Now decent into the dependency tree, level for level
+ do while(.not.self%finished())
+ call self%resolve(root, error)
+ if (allocated(error)) exit
+ end do
+ if (allocated(error)) return
+ end if
+ end associate
+
+ end subroutine update_dependency
+
+ !> Resolve all dependencies in the tree
+ subroutine resolve_dependencies(self, root, error)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(inout) :: self
+ !> Current installation prefix
+ character(len=*), intent(in) :: root
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: ii
+
+ do ii = 1, self%ndep
+ call self%resolve(self%dep(ii), root, error)
+ if (allocated(error)) exit
+ end do
+
+ if (allocated(error)) return
+
+ end subroutine resolve_dependencies
+
+ !> Resolve a single dependency node
+ subroutine resolve_dependency(self, dependency, root, error)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(inout) :: self
+ !> Dependency configuration to add
+ type(dependency_node_t), intent(inout) :: dependency
+ !> Current installation prefix
+ character(len=*), intent(in) :: root
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(package_config_t) :: package
+ character(len=:), allocatable :: manifest, proj_dir, revision
+ logical :: fetch
+
+ if (dependency%done) return
+
+ fetch = .false.
+ if (allocated(dependency%proj_dir)) then
+ proj_dir = dependency%proj_dir
+ else
+ if (allocated(dependency%path)) then
+ proj_dir = join_path(root, dependency%path)
+ else if (allocated(dependency%git)) then
+ proj_dir = join_path(self%dep_dir, dependency%name)
+ fetch = .not.exists(proj_dir)
+ if (fetch) then
+ call dependency%git%checkout(proj_dir, error)
+ if (allocated(error)) return
+ end if
+
+ end if
+ end if
+
+ if (allocated(dependency%git)) then
+ call git_revision(proj_dir, revision, error)
+ if (allocated(error)) return
+ end if
+
+ manifest = join_path(proj_dir, "fpm.toml")
+ call get_package_data(package, manifest, error)
+ if (allocated(error)) return
+
+ call dependency%register(package, proj_dir, fetch, revision, error)
+ if (allocated(error)) return
+
+ if (self%verbosity > 1) then
+ write(self%unit, out_fmt) &
+ "Dep:", dependency%name, "version", char(dependency%version), &
+ "at", dependency%proj_dir
+ end if
+
+ call self%add(package, proj_dir, .false., error)
+ if (allocated(error)) return
+
+ end subroutine resolve_dependency
+
+ !> Find a dependency in the dependency tree
+ pure function find_dependency(self, dependency) result(pos)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(in) :: self
+ !> Dependency configuration to add
+ class(dependency_config_t), intent(in) :: dependency
+ !> Index of the dependency
+ integer :: pos
+
+ integer :: ii
+
+ pos = self%find(dependency%name)
+
+ end function find_dependency
+
+ !> Find a dependency in the dependency tree
+ pure function find_name(self, name) result(pos)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(in) :: self
+ !> Dependency configuration to add
+ character(len=*), intent(in) :: name
+ !> Index of the dependency
+ integer :: pos
+
+ integer :: ii
+
+ pos = 0
+ do ii = 1, self%ndep
+ if (name == self%dep(ii)%name) then
+ pos = ii
+ exit
+ end if
+ end do
+
+ end function find_name
+
+ !> Check if we are done with the dependency resolution
+ pure function finished(self)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(in) :: self
+ !> All dependencies are updated
+ logical :: finished
+ integer :: ii
+
+ finished = all(self%dep(:self%ndep)%done)
+
+ end function finished
+
+ !> Update dependency from project manifest
+ subroutine register(self, package, root, fetch, revision, error)
+ !> Instance of the dependency node
+ class(dependency_node_t), intent(inout) :: self
+ !> Package configuration data
+ type(package_config_t), intent(in) :: package
+ !> Project has been fetched
+ logical, intent(in) :: fetch
+ !> Root directory of the project
+ character(len=*), intent(in) :: root
+ !> Git revision of the project
+ character(len=*), intent(in), optional :: revision
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ character(len=:), allocatable :: url
+ logical :: update
+
+ update = .false.
+ if (self%name /= package%name) then
+ call fatal_error(error, "Dependency name '"//package%name// &
+ & "' found, but expected '"//self%name//"' instead")
+ end if
+
+ self%version = package%version
+ self%proj_dir = root
+
+ if (allocated(self%git).and.present(revision)) then
+ self%revision = revision
+ if (.not.fetch) then
+ ! git object is HEAD always allows an update
+ update = .not.allocated(self%git%object)
+ if (.not.update) then
+ ! allow update in case the revision does not match the requested object
+ update = revision /= self%git%object
+ end if
+ end if
+ end if
+
+ self%update = update
+ self%done = .true.
+
+ end subroutine register
+
+ !> Read dependency tree from file
+ subroutine load_from_file(self, file, error)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(inout) :: self
+ !> File name
+ character(len=*), intent(in) :: file
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: unit
+ logical :: exist
+
+ inquire(file=file, exist=exist)
+ if (.not.exist) return
+
+ open(file=file, newunit=unit)
+ call self%load(unit, error)
+ close(unit)
+ end subroutine load_from_file
+
+ !> Read dependency tree from file
+ subroutine load_from_unit(self, unit, error)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(inout) :: self
+ !> File name
+ integer, intent(in) :: unit
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_error), allocatable :: parse_error
+ type(toml_table), allocatable :: table
+
+ call toml_parse(table, unit, parse_error)
+
+ if (allocated(parse_error)) then
+ allocate(error)
+ call move_alloc(parse_error%message, error%message)
+ return
+ end if
+
+ call self%load(table, error)
+ if (allocated(error)) return
+
+ end subroutine load_from_unit
+
+ !> Read dependency tree from TOML data structure
+ subroutine load_from_toml(self, table, error)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(inout) :: self
+ !> Data structure
+ type(toml_table), intent(inout) :: table
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: ndep, ii
+ logical :: unix
+ character(len=:), allocatable :: version, url, obj, rev, proj_dir
+ type(toml_key), allocatable :: list(:)
+ type(toml_table), pointer :: ptr
+ type(dependency_config_t) :: dep
+
+ call table%get_keys(list)
+
+ ndep = size(self%dep)
+ if (ndep < size(list) + self%ndep) then
+ call resize(self%dep, ndep + ndep/2 + size(list))
+ end if
+
+ unix = get_os_type() /= OS_WINDOWS
+
+ do ii = 1, size(list)
+ call get_value(table, list(ii)%key, ptr)
+ call get_value(ptr, "version", version)
+ call get_value(ptr, "proj-dir", proj_dir)
+ call get_value(ptr, "git", url)
+ call get_value(ptr, "obj", obj)
+ call get_value(ptr, "rev", rev)
+ if (.not.allocated(proj_dir)) cycle
+ self%ndep = self%ndep + 1
+ associate(dep => self%dep(self%ndep))
+ dep%name = list(ii)%key
+ if (unix) then
+ dep%proj_dir = proj_dir
+ else
+ dep%proj_dir = windows_path(proj_dir)
+ end if
+ dep%done = .false.
+ if (allocated(version)) then
+ if (.not.allocated(dep%version)) allocate(dep%version)
+ call new_version(dep%version, version, error)
+ if (allocated(error)) exit
+ end if
+ if (allocated(version)) then
+ call new_version(dep%version, version, error)
+ if (allocated(error)) exit
+ end if
+ if (allocated(url)) then
+ if (allocated(obj)) then
+ dep%git = git_target_revision(url, obj)
+ else
+ dep%git = git_target_default(url)
+ end if
+ if (allocated(rev)) then
+ dep%revision = rev
+ end if
+ else
+ dep%path = proj_dir
+ end if
+ end associate
+ end do
+ if (allocated(error)) return
+
+ self%ndep = size(list)
+ end subroutine load_from_toml
+
+ !> Write dependency tree to file
+ subroutine dump_to_file(self, file, error)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(inout) :: self
+ !> File name
+ character(len=*), intent(in) :: file
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: unit
+
+ open(file=file, newunit=unit)
+ call self%dump(unit, error)
+ close(unit)
+ if (allocated(error)) return
+
+ end subroutine dump_to_file
+
+ !> Write dependency tree to file
+ subroutine dump_to_unit(self, unit, error)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(inout) :: self
+ !> Formatted unit
+ integer, intent(in) :: unit
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_table) :: table
+ type(toml_serializer) :: ser
+
+ table = toml_table()
+ ser = toml_serializer(unit)
+
+ call self%dump(table, error)
+
+ call table%accept(ser)
+
+ end subroutine dump_to_unit
+
+ !> Write dependency tree to TOML datastructure
+ subroutine dump_to_toml(self, table, error)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(inout) :: self
+ !> Data structure
+ type(toml_table), intent(inout) :: table
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: ii
+ type(toml_table), pointer :: ptr
+ character(len=:), allocatable :: proj_dir
+
+ do ii = 1, self%ndep
+ associate(dep => self%dep(ii))
+ call add_table(table, dep%name, ptr)
+ if (.not.associated(ptr)) then
+ call fatal_error(error, "Cannot create entry for "//dep%name)
+ exit
+ end if
+ if (allocated(dep%version)) then
+ call set_value(ptr, "version", char(dep%version))
+ end if
+ proj_dir = canon_path(dep%proj_dir)
+ call set_value(ptr, "proj-dir", proj_dir)
+ if (allocated(dep%git)) then
+ call set_value(ptr, "git", dep%git%url)
+ if (allocated(dep%git%object)) then
+ call set_value(ptr, "obj", dep%git%object)
+ end if
+ if (allocated(dep%revision)) then
+ call set_value(ptr, "rev", dep%revision)
+ end if
+ end if
+ end associate
+ end do
+ if (allocated(error)) return
+
+ end subroutine dump_to_toml
+
+ !> Reallocate a list of dependencies
+ pure subroutine resize_dependency_node(var, n)
+ !> Instance of the array to be resized
+ type(dependency_node_t), allocatable, intent(inout) :: var(:)
+ !> Dimension of the final array size
+ integer, intent(in), optional :: n
+
+ type(dependency_node_t), allocatable :: tmp(:)
+ integer :: this_size, new_size
+ integer, parameter :: initial_size = 16
+
+ if (allocated(var)) then
+ this_size = size(var, 1)
+ call move_alloc(var, tmp)
+ else
+ this_size = initial_size
+ end if
+
+ if (present(n)) then
+ new_size = n
+ else
+ new_size = this_size + this_size/2 + 1
+ end if
+
+ allocate(var(new_size))
+
+ if (allocated(tmp)) then
+ this_size = min(size(tmp, 1), size(var, 1))
+ var(:this_size) = tmp(:this_size)
+ deallocate(tmp)
+ end if
+
+ end subroutine resize_dependency_node
+
+end module fpm_dependency
diff --git a/src/fpm/error.f90 b/src/fpm/error.f90
new file mode 100644
index 0000000..e69ff1e
--- /dev/null
+++ b/src/fpm/error.f90
@@ -0,0 +1,128 @@
+!> Implementation of basic error handling.
+module fpm_error
+ implicit none
+ private
+
+ public :: error_t
+ public :: fatal_error, syntax_error, file_not_found_error
+ public :: file_parse_error
+
+
+ !> Data type defining an error
+ type :: error_t
+
+ !> Error message
+ character(len=:), allocatable :: message
+
+ end type error_t
+
+
+ !> Alias syntax errors to fatal errors for now
+ interface syntax_error
+ module procedure :: fatal_error
+ end interface syntax_error
+
+
+contains
+
+
+ !> Generic fatal runtime error
+ subroutine fatal_error(error, message)
+
+ !> Instance of the error data
+ type(error_t), allocatable, intent(out) :: error
+
+ !> Error message
+ character(len=*), intent(in) :: message
+
+ allocate(error)
+ error%message = message
+
+ end subroutine fatal_error
+
+
+ !> Error created when a file is missing or not found
+ subroutine file_not_found_error(error, file_name)
+
+ !> Instance of the error data
+ type(error_t), allocatable, intent(out) :: error
+
+ !> Name of the missing file
+ character(len=*), intent(in) :: file_name
+
+ allocate(error)
+ error%message = "'"//file_name//"' could not be found, check if the file exists"
+
+ end subroutine file_not_found_error
+
+
+ !> Error created when file parsing fails
+ subroutine file_parse_error(error, file_name, message, line_num, &
+ line_string, line_col)
+
+ !> Instance of the error data
+ type(error_t), allocatable, intent(out) :: error
+
+ !> Name of file
+ character(len=*), intent(in) :: file_name
+
+ !> Parse error message
+ character(len=*), intent(in) :: message
+
+ !> Line number of parse error
+ integer, intent(in), optional :: line_num
+
+ !> Line context string
+ character(len=*), intent(in), optional :: line_string
+
+ !> Line context column
+ integer, intent(in), optional :: line_col
+
+ character(50) :: temp_string
+
+ allocate(error)
+ error%message = 'Parse error: '//message//new_line('a')
+
+ error%message = error%message//file_name
+
+ if (present(line_num)) then
+
+ write(temp_string,'(I0)') line_num
+
+ error%message = error%message//':'//trim(temp_string)
+
+ end if
+
+ if (present(line_col)) then
+
+ if (line_col > 0) then
+
+ write(temp_string,'(I0)') line_col
+ error%message = error%message//':'//trim(temp_string)
+
+ end if
+
+ end if
+
+ if (present(line_string)) then
+
+ error%message = error%message//new_line('a')
+ error%message = error%message//' | '//line_string
+
+ if (present(line_col)) then
+
+ if (line_col > 0) then
+
+ error%message = error%message//new_line('a')
+ error%message = error%message//' | '//repeat(' ',line_col-1)//'^'
+
+ end if
+
+ end if
+
+ end if
+
+ end subroutine file_parse_error
+
+
+end module fpm_error
diff --git a/src/fpm/git.f90 b/src/fpm/git.f90
new file mode 100644
index 0000000..08e27b2
--- /dev/null
+++ b/src/fpm/git.f90
@@ -0,0 +1,263 @@
+!> Implementation for interacting with git repositories.
+module fpm_git
+ use fpm_error, only: error_t, fatal_error
+ use fpm_filesystem, only : get_temp_filename, getline
+ implicit none
+
+ public :: git_target_t
+ public :: git_target_default, git_target_branch, git_target_tag, &
+ & git_target_revision
+ public :: git_revision
+
+
+ !> Possible git target
+ type :: enum_descriptor
+
+ !> Default target
+ integer :: default = 200
+
+ !> Branch in git repository
+ integer :: branch = 201
+
+ !> Tag in git repository
+ integer :: tag = 202
+
+ !> Commit hash
+ integer :: revision = 203
+
+ end type enum_descriptor
+
+ !> Actual enumerator for descriptors
+ type(enum_descriptor), parameter :: git_descriptor = enum_descriptor()
+
+
+ !> Description of an git target
+ type :: git_target_t
+
+ !> Kind of the git target
+ integer, private :: descriptor = git_descriptor%default
+
+ !> Target URL of the git repository
+ character(len=:), allocatable :: url
+
+ !> Additional descriptor of the git object
+ character(len=:), allocatable :: object
+
+ contains
+
+ !> Fetch and checkout in local directory
+ procedure :: checkout
+
+ !> Show information on instance
+ procedure :: info
+
+ end type git_target_t
+
+
+contains
+
+
+ !> Default target
+ function git_target_default(url) result(self)
+
+ !> Target URL of the git repository
+ character(len=*), intent(in) :: url
+
+ !> New git target
+ type(git_target_t) :: self
+
+ self%descriptor = git_descriptor%default
+ self%url = url
+
+ end function git_target_default
+
+
+ !> Target a branch in the git repository
+ function git_target_branch(url, branch) result(self)
+
+ !> Target URL of the git repository
+ character(len=*), intent(in) :: url
+
+ !> Name of the branch of interest
+ character(len=*), intent(in) :: branch
+
+ !> New git target
+ type(git_target_t) :: self
+
+ self%descriptor = git_descriptor%branch
+ self%url = url
+ self%object = branch
+
+ end function git_target_branch
+
+
+ !> Target a specific git revision
+ function git_target_revision(url, sha1) result(self)
+
+ !> Target URL of the git repository
+ character(len=*), intent(in) :: url
+
+ !> Commit hash of interest
+ character(len=*), intent(in) :: sha1
+
+ !> New git target
+ type(git_target_t) :: self
+
+ self%descriptor = git_descriptor%revision
+ self%url = url
+ self%object = sha1
+
+ end function git_target_revision
+
+
+ !> Target a git tag
+ function git_target_tag(url, tag) result(self)
+
+ !> Target URL of the git repository
+ character(len=*), intent(in) :: url
+
+ !> Tag name of interest
+ character(len=*), intent(in) :: tag
+
+ !> New git target
+ type(git_target_t) :: self
+
+ self%descriptor = git_descriptor%tag
+ self%url = url
+ self%object = tag
+
+ end function git_target_tag
+
+
+ subroutine checkout(self, local_path, error)
+
+ !> Instance of the git target
+ class(git_target_t), intent(in) :: self
+
+ !> Local path to checkout in
+ character(*), intent(in) :: local_path
+
+ !> Error
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: stat
+ character(len=:), allocatable :: object
+
+ if (allocated(self%object)) then
+ object = self%object
+ else
+ object = 'HEAD'
+ end if
+
+ call execute_command_line("git init "//local_path, exitstat=stat)
+
+ if (stat /= 0) then
+ call fatal_error(error,'Error while initiating git repository for remote dependency')
+ return
+ end if
+
+ call execute_command_line("git -C "//local_path//" fetch --depth=1 "// &
+ self%url//" "//object, exitstat=stat)
+
+ if (stat /= 0) then
+ call fatal_error(error,'Error while fetching git repository for remote dependency')
+ return
+ end if
+
+ call execute_command_line("git -C "//local_path//" checkout -qf FETCH_HEAD", exitstat=stat)
+
+ if (stat /= 0) then
+ call fatal_error(error,'Error while checking out git repository for remote dependency')
+ return
+ end if
+
+ end subroutine checkout
+
+
+ subroutine git_revision(local_path, object, error)
+
+ !> Local path to checkout in
+ character(*), intent(in) :: local_path
+
+ !> Git object reference
+ character(len=:), allocatable, intent(out) :: object
+
+ !> Error
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: stat, unit, istart, iend
+ character(len=:), allocatable :: temp_file, line, iomsg
+ character(len=*), parameter :: hexdigits = '0123456789abcdef'
+
+ allocate(temp_file, source=get_temp_filename())
+ line = "git -C "//local_path//" log -n 1 > "//temp_file
+ call execute_command_line(line, exitstat=stat)
+
+ if (stat /= 0) then
+ call fatal_error(error, "Error while retrieving commit information")
+ return
+ end if
+
+ open(file=temp_file, newunit=unit)
+ call getline(unit, line, stat, iomsg)
+
+ if (stat /= 0) then
+ call fatal_error(error, iomsg)
+ return
+ end if
+ close(unit, status="delete")
+
+ ! Tokenize:
+ ! commit 0123456789abcdef (HEAD, ...)
+ istart = scan(line, ' ') + 1
+ iend = verify(line(istart:), hexdigits) + istart - 1
+ if (iend < istart) iend = len(line)
+ object = line(istart:iend)
+
+ end subroutine git_revision
+
+
+ !> Show information on git target
+ subroutine info(self, unit, verbosity)
+
+ !> Instance of the git target
+ class(git_target_t), intent(in) :: self
+
+ !> Unit for IO
+ integer, intent(in) :: unit
+
+ !> Verbosity of the printout
+ integer, intent(in), optional :: verbosity
+
+ integer :: pr
+ character(len=*), parameter :: fmt = '("#", 1x, a, t30, a)'
+
+ if (present(verbosity)) then
+ pr = verbosity
+ else
+ pr = 1
+ end if
+
+ if (pr < 1) return
+
+ write(unit, fmt) "Git target"
+ if (allocated(self%url)) then
+ write(unit, fmt) "- URL", self%url
+ end if
+ if (allocated(self%object)) then
+ select case(self%descriptor)
+ case default
+ write(unit, fmt) "- object", self%object
+ case(git_descriptor%tag)
+ write(unit, fmt) "- tag", self%object
+ case(git_descriptor%branch)
+ write(unit, fmt) "- branch", self%object
+ case(git_descriptor%revision)
+ write(unit, fmt) "- sha1", self%object
+ end select
+ end if
+
+ end subroutine info
+
+
+end module fpm_git
diff --git a/src/fpm/installer.f90 b/src/fpm/installer.f90
new file mode 100644
index 0000000..d01bd27
--- /dev/null
+++ b/src/fpm/installer.f90
@@ -0,0 +1,284 @@
+!> Implementation of an installer object.
+!>
+!> The installer provides a way to install objects to their respective directories
+!> in the installation prefix, a generic install command allows to install
+!> to any directory within the prefix.
+module fpm_installer
+ use, intrinsic :: iso_fortran_env, only : output_unit
+ use fpm_environment, only : get_os_type, os_is_unix
+ use fpm_error, only : error_t, fatal_error
+ use fpm_filesystem, only : join_path, mkdir, exists, unix_path, windows_path, &
+ env_variable
+ implicit none
+ private
+
+ public :: installer_t, new_installer
+
+
+ !> Declaration of the installer type
+ type :: installer_t
+ !> Path to installation directory
+ character(len=:), allocatable :: prefix
+ !> Binary dir relative to the installation prefix
+ character(len=:), allocatable :: bindir
+ !> Library directory relative to the installation prefix
+ character(len=:), allocatable :: libdir
+ !> Include directory relative to the installation prefix
+ character(len=:), allocatable :: includedir
+ !> Output unit for informative printout
+ integer :: unit = output_unit
+ !> Verbosity of the installer
+ integer :: verbosity = 1
+ !> Command to copy objects into the installation prefix
+ character(len=:), allocatable :: copy
+ !> Cached operating system
+ integer :: os
+ contains
+ !> Install an executable in its correct subdirectory
+ procedure :: install_executable
+ !> Install a library in its correct subdirectory
+ procedure :: install_library
+ !> Install a header/module in its correct subdirectory
+ procedure :: install_header
+ !> Install a generic file into a subdirectory in the installation prefix
+ procedure :: install
+ !> Run an installation command, type-bound for unit testing purposes
+ procedure :: run
+ !> Create a new directory in the prefix, type-bound for unit testing purposes
+ procedure :: make_dir
+ end type installer_t
+
+ !> Default name of the binary subdirectory
+ character(len=*), parameter :: default_bindir = "bin"
+
+ !> Default name of the library subdirectory
+ character(len=*), parameter :: default_libdir = "lib"
+
+ !> Default name of the include subdirectory
+ character(len=*), parameter :: default_includedir = "include"
+
+ !> Default name of the installation prefix on Unix platforms
+ character(len=*), parameter :: default_prefix_unix = "/usr/local"
+
+ !> Default name of the installation prefix on Windows platforms
+ character(len=*), parameter :: default_prefix_win = "C:\"
+
+ !> Copy command on Unix platforms
+ character(len=*), parameter :: default_copy_unix = "cp"
+
+ !> Copy command on Windows platforms
+ character(len=*), parameter :: default_copy_win = "copy"
+
+contains
+
+ !> Create a new instance of an installer
+ subroutine new_installer(self, prefix, bindir, libdir, includedir, verbosity, &
+ copy)
+ !> Instance of the installer
+ type(installer_t), intent(out) :: self
+ !> Path to installation directory
+ character(len=*), intent(in), optional :: prefix
+ !> Binary dir relative to the installation prefix
+ character(len=*), intent(in), optional :: bindir
+ !> Library directory relative to the installation prefix
+ character(len=*), intent(in), optional :: libdir
+ !> Include directory relative to the installation prefix
+ character(len=*), intent(in), optional :: includedir
+ !> Verbosity of the installer
+ integer, intent(in), optional :: verbosity
+ !> Copy command
+ character(len=*), intent(in), optional :: copy
+
+ self%os = get_os_type()
+
+ if (present(copy)) then
+ self%copy = copy
+ else
+ if (os_is_unix(self%os)) then
+ self%copy = default_copy_unix
+ else
+ self%copy = default_copy_win
+ end if
+ end if
+
+ if (present(includedir)) then
+ self%includedir = includedir
+ else
+ self%includedir = default_includedir
+ end if
+
+ if (present(prefix)) then
+ self%prefix = prefix
+ else
+ call set_default_prefix(self%prefix, self%os)
+ end if
+
+ if (present(bindir)) then
+ self%bindir = bindir
+ else
+ self%bindir = default_bindir
+ end if
+
+ if (present(libdir)) then
+ self%libdir = libdir
+ else
+ self%libdir = default_libdir
+ end if
+
+ if (present(verbosity)) then
+ self%verbosity = verbosity
+ else
+ self%verbosity = 1
+ end if
+
+ end subroutine new_installer
+
+ !> Set the default prefix for the installation
+ subroutine set_default_prefix(prefix, os)
+ !> Installation prefix
+ character(len=:), allocatable :: prefix
+ !> Platform identifier
+ integer, intent(in), optional :: os
+
+ character(len=:), allocatable :: home
+
+ if (os_is_unix(os)) then
+ call env_variable(home, "HOME")
+ if (allocated(home)) then
+ prefix = join_path(home, ".local")
+ else
+ prefix = default_prefix_unix
+ end if
+ else
+ call env_variable(home, "APPDATA")
+ if (allocated(home)) then
+ prefix = join_path(home, "local")
+ else
+ prefix = default_prefix_win
+ end if
+ end if
+
+ end subroutine set_default_prefix
+
+ !> Install an executable in its correct subdirectory
+ subroutine install_executable(self, executable, error)
+ !> Instance of the installer
+ class(installer_t), intent(inout) :: self
+ !> Path to the executable
+ character(len=*), intent(in) :: executable
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+ integer :: ll
+
+ if (.not.os_is_unix(self%os)) then
+ ll = len(executable)
+ if (executable(max(1, ll-3):ll) /= ".exe") then
+ call self%install(executable//".exe", self%bindir, error)
+ return
+ end if
+ end if
+
+ call self%install(executable, self%bindir, error)
+
+ end subroutine install_executable
+
+ !> Install a library in its correct subdirectory
+ subroutine install_library(self, library, error)
+ !> Instance of the installer
+ class(installer_t), intent(inout) :: self
+ !> Path to the library
+ character(len=*), intent(in) :: library
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ call self%install(library, self%libdir, error)
+ end subroutine install_library
+
+ !> Install a header/module in its correct subdirectory
+ subroutine install_header(self, header, error)
+ !> Instance of the installer
+ class(installer_t), intent(inout) :: self
+ !> Path to the header
+ character(len=*), intent(in) :: header
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ call self%install(header, self%includedir, error)
+ end subroutine install_header
+
+ !> Install a generic file into a subdirectory in the installation prefix
+ subroutine install(self, source, destination, error)
+ !> Instance of the installer
+ class(installer_t), intent(inout) :: self
+ !> Path to the original file
+ character(len=*), intent(in) :: source
+ !> Path to the destination inside the prefix
+ character(len=*), intent(in) :: destination
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ character(len=:), allocatable :: install_dest
+
+ install_dest = join_path(self%prefix, destination)
+ if (os_is_unix(self%os)) then
+ install_dest = unix_path(install_dest)
+ else
+ install_dest = windows_path(install_dest)
+ end if
+ call self%make_dir(install_dest, error)
+ if (allocated(error)) return
+
+ if (self%verbosity > 0) then
+ if (exists(install_dest)) then
+ write(self%unit, '("# Update:", 1x, a, 1x, "->", 1x, a)') &
+ source, install_dest
+ else
+ write(self%unit, '("# Install:", 1x, a, 1x, "->", 1x, a)') &
+ source, install_dest
+ end if
+ end if
+
+ call self%run(self%copy//' "'//source//'" "'//install_dest//'"', error)
+ if (allocated(error)) return
+
+ end subroutine install
+
+ !> Create a new directory in the prefix
+ subroutine make_dir(self, dir, error)
+ !> Instance of the installer
+ class(installer_t), intent(inout) :: self
+ !> Directory to be created
+ character(len=*), intent(in) :: dir
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ if (.not.exists(dir)) then
+ if (self%verbosity > 1) then
+ write(self%unit, '("# Dir:", 1x, a)') dir
+ end if
+ call mkdir(dir)
+ end if
+ end subroutine make_dir
+
+ !> Run an installation command
+ subroutine run(self, command, error)
+ !> Instance of the installer
+ class(installer_t), intent(inout) :: self
+ !> Command to be launched
+ character(len=*), intent(in) :: command
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+ integer :: stat
+
+ if (self%verbosity > 1) then
+ write(self%unit, '("# Run:", 1x, a)') command
+ end if
+ call execute_command_line(command, exitstat=stat)
+
+ if (stat /= 0) then
+ call fatal_error(error, "Failed in command: '"//command//"'")
+ return
+ end if
+ end subroutine run
+
+end module fpm_installer
diff --git a/src/fpm/manifest.f90 b/src/fpm/manifest.f90
new file mode 100644
index 0000000..4170b91
--- /dev/null
+++ b/src/fpm/manifest.f90
@@ -0,0 +1,184 @@
+!> Package configuration data.
+!>
+!> This module provides the necessary procedure to translate a TOML document
+!> to the corresponding Fortran type, while verifying it with respect to
+!> its schema.
+!>
+!> Additionally, the required data types for users of this module are reexported
+!> to hide the actual implementation details.
+module fpm_manifest
+ use fpm_manifest_build, only: build_config_t
+ use fpm_manifest_example, only : example_config_t
+ use fpm_manifest_executable, only : executable_config_t
+ use fpm_manifest_dependency, only : dependency_config_t
+ use fpm_manifest_library, only : library_config_t
+ use fpm_manifest_package, only : package_config_t, new_package
+ use fpm_error, only : error_t, fatal_error, file_not_found_error
+ use fpm_toml, only : toml_table, read_package_file
+ use fpm_manifest_test, only : test_config_t
+ use fpm_filesystem, only: join_path, exists, dirname, is_dir
+ use fpm_strings, only: string_t
+ implicit none
+ private
+
+ public :: get_package_data, default_executable, default_library, default_test
+ public :: default_example
+ public :: package_config_t, dependency_config_t
+
+
+contains
+
+
+ !> Populate library in case we find the default src directory
+ subroutine default_library(self)
+
+ !> Instance of the library meta data
+ type(library_config_t), intent(out) :: self
+
+ self%source_dir = "src"
+ self%include_dir = [string_t("include")]
+
+ end subroutine default_library
+
+
+ !> Populate executable in case we find the default app directory
+ subroutine default_executable(self, name)
+
+ !> Instance of the executable meta data
+ type(executable_config_t), intent(out) :: self
+
+ !> Name of the package
+ character(len=*), intent(in) :: name
+
+ self%name = name
+ self%source_dir = "app"
+ self%main = "main.f90"
+
+ end subroutine default_executable
+
+ !> Populate test in case we find the default example/ directory
+ subroutine default_example(self, name)
+
+ !> Instance of the executable meta data
+ type(example_config_t), intent(out) :: self
+
+ !> Name of the package
+ character(len=*), intent(in) :: name
+
+ self%name = name // "-demo"
+ self%source_dir = "example"
+ self%main = "main.f90"
+
+ end subroutine default_example
+
+ !> Populate test in case we find the default test/ directory
+ subroutine default_test(self, name)
+
+ !> Instance of the executable meta data
+ type(test_config_t), intent(out) :: self
+
+ !> Name of the package
+ character(len=*), intent(in) :: name
+
+ self%name = name // "-test"
+ self%source_dir = "test"
+ self%main = "main.f90"
+
+ end subroutine default_test
+
+
+ !> Obtain package meta data from a configuation file
+ subroutine get_package_data(package, file, error, apply_defaults)
+
+ !> Parsed package meta data
+ type(package_config_t), intent(out) :: package
+
+ !> Name of the package configuration file
+ character(len=*), intent(in) :: file
+
+ !> Error status of the operation
+ type(error_t), allocatable, intent(out) :: error
+
+ !> Apply package defaults (uses file system operations)
+ logical, intent(in), optional :: apply_defaults
+
+ type(toml_table), allocatable :: table
+ character(len=:), allocatable :: root
+
+ call read_package_file(table, file, error)
+ if (allocated(error)) return
+
+ if (.not.allocated(table)) then
+ call fatal_error(error, "Unclassified error while reading: '"//file//"'")
+ return
+ end if
+
+ call new_package(package, table, error)
+ if (allocated(error)) return
+
+ if (present(apply_defaults)) then
+ if (apply_defaults) then
+ root = dirname(file)
+ if (len_trim(root) == 0) root = "."
+ call package_defaults(package, root, error)
+ if (allocated(error)) return
+ end if
+ end if
+
+ end subroutine get_package_data
+
+
+ !> Apply package defaults
+ subroutine package_defaults(package, root, error)
+
+ !> Parsed package meta data
+ type(package_config_t), intent(inout) :: package
+
+ !> Current working directory
+ character(len=*), intent(in) :: root
+
+ !> Error status of the operation
+ type(error_t), allocatable, intent(out) :: error
+
+ ! Populate library in case we find the default src directory
+ if (.not.allocated(package%library) .and. &
+ & (is_dir(join_path(root, "src")) .or. &
+ & is_dir(join_path(root, "include")))) then
+
+ allocate(package%library)
+ call default_library(package%library)
+ end if
+
+ ! Populate executable in case we find the default app
+ if (.not.allocated(package%executable) .and. &
+ & exists(join_path(root, "app", "main.f90"))) then
+ allocate(package%executable(1))
+ call default_executable(package%executable(1), package%name)
+ end if
+
+ ! Populate example in case we find the default example directory
+ if (.not.allocated(package%example) .and. &
+ & exists(join_path(root, "example", "main.f90"))) then
+ allocate(package%example(1))
+ call default_example(package%example(1), package%name)
+ endif
+
+ ! Populate test in case we find the default test directory
+ if (.not.allocated(package%test) .and. &
+ & exists(join_path(root, "test", "main.f90"))) then
+ allocate(package%test(1))
+ call default_test(package%test(1), package%name)
+ endif
+
+ if (.not.(allocated(package%library) &
+ & .or. allocated(package%executable) &
+ & .or. allocated(package%example) &
+ & .or. allocated(package%test))) then
+ call fatal_error(error, "Neither library nor executable found, there is nothing to do")
+ return
+ end if
+
+ end subroutine package_defaults
+
+
+end module fpm_manifest
diff --git a/src/fpm/manifest/build.f90 b/src/fpm/manifest/build.f90
new file mode 100644
index 0000000..d96974f
--- /dev/null
+++ b/src/fpm/manifest/build.f90
@@ -0,0 +1,162 @@
+!> Implementation of the build configuration data.
+!>
+!> A build table can currently have the following fields
+!>
+!>```toml
+!>[build]
+!>auto-executables = bool
+!>auto-examples = bool
+!>auto-tests = bool
+!>link = ["lib"]
+!>```
+module fpm_manifest_build
+ use fpm_error, only : error_t, syntax_error, fatal_error
+ use fpm_strings, only : string_t
+ use fpm_toml, only : toml_table, toml_key, toml_stat, get_value
+ implicit none
+ private
+
+ public :: build_config_t, new_build_config
+
+
+ !> Configuration data for build
+ type :: build_config_t
+
+ !> Automatic discovery of executables
+ logical :: auto_executables
+
+ !> Automatic discovery of examples
+ logical :: auto_examples
+
+ !> Automatic discovery of tests
+ logical :: auto_tests
+
+ !> Libraries to link against
+ type(string_t), allocatable :: link(:)
+
+ contains
+
+ !> Print information on this instance
+ procedure :: info
+
+ end type build_config_t
+
+
+contains
+
+
+ !> Construct a new build configuration from a TOML data structure
+ subroutine new_build_config(self, table, error)
+
+ !> Instance of the build configuration
+ type(build_config_t), intent(out) :: self
+
+ !> Instance of the TOML data structure
+ type(toml_table), intent(inout) :: table
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: stat
+
+ call check(table, error)
+ if (allocated(error)) return
+
+ call get_value(table, "auto-executables", self%auto_executables, .true., stat=stat)
+
+ if (stat /= toml_stat%success) then
+ call fatal_error(error,"Error while reading value for 'auto-executables' in fpm.toml, expecting logical")
+ return
+ end if
+
+ call get_value(table, "auto-tests", self%auto_tests, .true., stat=stat)
+
+ if (stat /= toml_stat%success) then
+ call fatal_error(error,"Error while reading value for 'auto-tests' in fpm.toml, expecting logical")
+ return
+ end if
+
+ call get_value(table, "auto-examples", self%auto_examples, .true., stat=stat)
+
+ if (stat /= toml_stat%success) then
+ call fatal_error(error,"Error while reading value for 'auto-examples' in fpm.toml, expecting logical")
+ return
+ end if
+
+
+ call get_value(table, "link", self%link, error)
+ if (allocated(error)) return
+
+ end subroutine new_build_config
+
+
+ !> Check local schema for allowed entries
+ subroutine check(table, error)
+
+ !> Instance of the TOML data structure
+ type(toml_table), intent(inout) :: table
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_key), allocatable :: list(:)
+ integer :: ikey
+
+ call table%get_keys(list)
+
+ ! table can be empty
+ if (size(list) < 1) return
+
+ do ikey = 1, size(list)
+ select case(list(ikey)%key)
+
+ case("auto-executables", "auto-examples", "auto-tests", "link")
+ continue
+
+ case default
+ call syntax_error(error, "Key "//list(ikey)%key//" is not allowed in [build]")
+ exit
+
+ end select
+ end do
+
+ end subroutine check
+
+
+ !> Write information on build configuration instance
+ subroutine info(self, unit, verbosity)
+
+ !> Instance of the build configuration
+ class(build_config_t), intent(in) :: self
+
+ !> Unit for IO
+ integer, intent(in) :: unit
+
+ !> Verbosity of the printout
+ integer, intent(in), optional :: verbosity
+
+ integer :: pr, ilink
+ character(len=*), parameter :: fmt = '("#", 1x, a, t30, a)'
+
+ if (present(verbosity)) then
+ pr = verbosity
+ else
+ pr = 1
+ end if
+
+ if (pr < 1) return
+
+ write(unit, fmt) "Build configuration"
+ write(unit, fmt) " - auto-discovery (apps) ", merge("enabled ", "disabled", self%auto_executables)
+ write(unit, fmt) " - auto-discovery (examples) ", merge("enabled ", "disabled", self%auto_examples)
+ write(unit, fmt) " - auto-discovery (tests) ", merge("enabled ", "disabled", self%auto_tests)
+ if (allocated(self%link)) then
+ write(unit, fmt) " - link against"
+ do ilink = 1, size(self%link)
+ write(unit, fmt) " - " // self%link(ilink)%s
+ end do
+ end if
+
+ end subroutine info
+
+end module fpm_manifest_build
diff --git a/src/fpm/manifest/dependency.f90 b/src/fpm/manifest/dependency.f90
new file mode 100644
index 0000000..26b76ee
--- /dev/null
+++ b/src/fpm/manifest/dependency.f90
@@ -0,0 +1,248 @@
+!> Implementation of the meta data for dependencies.
+!>
+!> A dependency table can currently have the following fields
+!>
+!>```toml
+!>[dependencies]
+!>"dep1" = { git = "url" }
+!>"dep2" = { git = "url", branch = "name" }
+!>"dep3" = { git = "url", tag = "name" }
+!>"dep4" = { git = "url", rev = "sha1" }
+!>"dep0" = { path = "path" }
+!>```
+!>
+!> To reduce the amount of boilerplate code this module provides two constructors
+!> for dependency types, one basic for an actual dependency (inline) table
+!> and another to collect all dependency objects from a dependencies table,
+!> which is handling the allocation of the objects and is forwarding the
+!> individual dependency tables to their respective constructors.
+!> The usual entry point should be the constructor for the super table.
+!>
+!> This objects contains a target to retrieve required `fpm` projects to
+!> build the target declaring the dependency.
+!> Resolving a dependency will result in obtaining a new package configuration
+!> data for the respective project.
+module fpm_manifest_dependency
+ use fpm_error, only : error_t, syntax_error
+ use fpm_git, only : git_target_t, git_target_tag, git_target_branch, &
+ & git_target_revision, git_target_default
+ use fpm_toml, only : toml_table, toml_key, toml_stat, get_value
+ implicit none
+ private
+
+ public :: dependency_config_t, new_dependency, new_dependencies
+
+
+ !> Configuration meta data for a dependency
+ type :: dependency_config_t
+
+ !> Name of the dependency
+ character(len=:), allocatable :: name
+
+ !> Local target
+ character(len=:), allocatable :: path
+
+ !> Git descriptor
+ type(git_target_t), allocatable :: git
+
+ contains
+
+ !> Print information on this instance
+ procedure :: info
+
+ end type dependency_config_t
+
+
+contains
+
+
+ !> Construct a new dependency configuration from a TOML data structure
+ subroutine new_dependency(self, table, error)
+
+ !> Instance of the dependency configuration
+ type(dependency_config_t), intent(out) :: self
+
+ !> Instance of the TOML data structure
+ type(toml_table), intent(inout) :: table
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ character(len=:), allocatable :: url, obj
+
+ call check(table, error)
+ if (allocated(error)) return
+
+ call table%get_key(self%name)
+
+ call get_value(table, "path", url)
+ if (allocated(url)) then
+ call move_alloc(url, self%path)
+ else
+ call get_value(table, "git", url)
+
+ call get_value(table, "tag", obj)
+ if (allocated(obj)) then
+ self%git = git_target_tag(url, obj)
+ end if
+
+ if (.not.allocated(self%git)) then
+ call get_value(table, "branch", obj)
+ if (allocated(obj)) then
+ self%git = git_target_branch(url, obj)
+ end if
+ end if
+
+ if (.not.allocated(self%git)) then
+ call get_value(table, "rev", obj)
+ if (allocated(obj)) then
+ self%git = git_target_revision(url, obj)
+ end if
+ end if
+
+ if (.not.allocated(self%git)) then
+ self%git = git_target_default(url)
+ end if
+
+ end if
+
+ end subroutine new_dependency
+
+
+ !> Check local schema for allowed entries
+ subroutine check(table, error)
+
+ !> Instance of the TOML data structure
+ type(toml_table), intent(inout) :: table
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ character(len=:), allocatable :: name
+ type(toml_key), allocatable :: list(:)
+ logical :: url_present, git_target_present, has_path
+ integer :: ikey
+
+ has_path = .false.
+ url_present = .false.
+ git_target_present = .false.
+
+ call table%get_key(name)
+ call table%get_keys(list)
+
+ if (size(list) < 1) then
+ call syntax_error(error, "Dependency "//name//" does not provide sufficient entries")
+ return
+ end if
+
+ do ikey = 1, size(list)
+ select case(list(ikey)%key)
+ case default
+ call syntax_error(error, "Key "//list(ikey)%key//" is not allowed in dependency "//name)
+ exit
+
+ case("git", "path")
+ if (url_present) then
+ call syntax_error(error, "Dependency "//name//" cannot have both git and path entries")
+ exit
+ end if
+ url_present = .true.
+ has_path = list(ikey)%key == 'path'
+
+ case("branch", "rev", "tag")
+ if (git_target_present) then
+ call syntax_error(error, "Dependency "//name//" can only have one of branch, rev or tag present")
+ exit
+ end if
+ git_target_present = .true.
+
+ end select
+ end do
+ if (allocated(error)) return
+
+ if (.not.url_present) then
+ call syntax_error(error, "Dependency "//name//" does not provide a method to actually retrieve itself")
+ return
+ end if
+
+ if (has_path .and. git_target_present) then
+ call syntax_error(error, "Dependency "//name//" uses a local path, therefore no git identifiers are allowed")
+ end if
+
+ end subroutine check
+
+
+ !> Construct new dependency array from a TOML data structure
+ subroutine new_dependencies(deps, table, error)
+
+ !> Instance of the dependency configuration
+ type(dependency_config_t), allocatable, intent(out) :: deps(:)
+
+ !> Instance of the TOML data structure
+ type(toml_table), intent(inout) :: table
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_table), pointer :: node
+ type(toml_key), allocatable :: list(:)
+ integer :: idep, stat
+
+ call table%get_keys(list)
+ ! An empty table is okay
+ if (size(list) < 1) return
+
+ allocate(deps(size(list)))
+ do idep = 1, size(list)
+ call get_value(table, list(idep)%key, node, stat=stat)
+ if (stat /= toml_stat%success) then
+ call syntax_error(error, "Dependency "//list(idep)%key//" must be a table entry")
+ exit
+ end if
+ call new_dependency(deps(idep), node, error)
+ if (allocated(error)) exit
+ end do
+
+ end subroutine new_dependencies
+
+
+ !> Write information on instance
+ subroutine info(self, unit, verbosity)
+
+ !> Instance of the dependency configuration
+ class(dependency_config_t), intent(in) :: self
+
+ !> Unit for IO
+ integer, intent(in) :: unit
+
+ !> Verbosity of the printout
+ integer, intent(in), optional :: verbosity
+
+ integer :: pr
+ character(len=*), parameter :: fmt = '("#", 1x, a, t30, a)'
+
+ if (present(verbosity)) then
+ pr = verbosity
+ else
+ pr = 1
+ end if
+
+ write(unit, fmt) "Dependency"
+ if (allocated(self%name)) then
+ write(unit, fmt) "- name", self%name
+ end if
+
+ if (allocated(self%git)) then
+ write(unit, fmt) "- kind", "git"
+ call self%git%info(unit, pr - 1)
+ end if
+
+ if (allocated(self%path)) then
+ write(unit, fmt) "- kind", "local"
+ write(unit, fmt) "- path", self%path
+ end if
+
+ end subroutine info
+
+
+end module fpm_manifest_dependency
diff --git a/src/fpm/manifest/example.f90 b/src/fpm/manifest/example.f90
new file mode 100644
index 0000000..fc2a0af
--- /dev/null
+++ b/src/fpm/manifest/example.f90
@@ -0,0 +1,175 @@
+!> Implementation of the meta data for an example.
+!>
+!> The example data structure is effectively a decorated version of an executable
+!> and shares most of its properties, except for the defaults and can be
+!> handled under most circumstances just like any other executable.
+!>
+!> A example table can currently have the following fields
+!>
+!>```toml
+!>[[ example ]]
+!>name = "string"
+!>source-dir = "path"
+!>main = "file"
+!>link = ["lib"]
+!>[example.dependencies]
+!>```
+module fpm_manifest_example
+ use fpm_manifest_dependency, only : dependency_config_t, new_dependencies
+ use fpm_manifest_executable, only : executable_config_t
+ use fpm_error, only : error_t, syntax_error
+ use fpm_toml, only : toml_table, toml_key, toml_stat, get_value
+ implicit none
+ private
+
+ public :: example_config_t, new_example
+
+
+ !> Configuation meta data for an example
+ type, extends(executable_config_t) :: example_config_t
+
+ contains
+
+ !> Print information on this instance
+ procedure :: info
+
+ end type example_config_t
+
+
+contains
+
+
+ !> Construct a new example configuration from a TOML data structure
+ subroutine new_example(self, table, error)
+
+ !> Instance of the example configuration
+ type(example_config_t), intent(out) :: self
+
+ !> Instance of the TOML data structure
+ type(toml_table), intent(inout) :: table
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_table), pointer :: child
+
+ call check(table, error)
+ if (allocated(error)) return
+
+ call get_value(table, "name", self%name)
+ if (.not.allocated(self%name)) then
+ call syntax_error(error, "Could not retrieve example name")
+ return
+ end if
+ call get_value(table, "source-dir", self%source_dir, "example")
+ call get_value(table, "main", self%main, "main.f90")
+
+ call get_value(table, "dependencies", child, requested=.false.)
+ if (associated(child)) then
+ call new_dependencies(self%dependency, child, error)
+ if (allocated(error)) return
+ end if
+
+ call get_value(table, "link", self%link, error)
+ if (allocated(error)) return
+
+ end subroutine new_example
+
+
+ !> Check local schema for allowed entries
+ subroutine check(table, error)
+
+ !> Instance of the TOML data structure
+ type(toml_table), intent(inout) :: table
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_key), allocatable :: list(:)
+ logical :: name_present
+ integer :: ikey
+
+ name_present = .false.
+
+ call table%get_keys(list)
+
+ if (size(list) < 1) then
+ call syntax_error(error, "Example section does not provide sufficient entries")
+ return
+ end if
+
+ do ikey = 1, size(list)
+ select case(list(ikey)%key)
+ case default
+ call syntax_error(error, "Key "//list(ikey)%key//" is not allowed in example entry")
+ exit
+
+ case("name")
+ name_present = .true.
+
+ case("source-dir", "main", "dependencies", "link")
+ continue
+
+ end select
+ end do
+ if (allocated(error)) return
+
+ if (.not.name_present) then
+ call syntax_error(error, "Example name is not provided, please add a name entry")
+ end if
+
+ end subroutine check
+
+
+ !> Write information on instance
+ subroutine info(self, unit, verbosity)
+
+ !> Instance of the example configuration
+ class(example_config_t), intent(in) :: self
+
+ !> Unit for IO
+ integer, intent(in) :: unit
+
+ !> Verbosity of the printout
+ integer, intent(in), optional :: verbosity
+
+ integer :: pr, ii
+ character(len=*), parameter :: fmt = '("#", 1x, a, t30, a)', &
+ & fmti = '("#", 1x, a, t30, i0)'
+
+ if (present(verbosity)) then
+ pr = verbosity
+ else
+ pr = 1
+ end if
+
+ if (pr < 1) return
+
+ write(unit, fmt) "Example target"
+ if (allocated(self%name)) then
+ write(unit, fmt) "- name", self%name
+ end if
+ if (allocated(self%source_dir)) then
+ if (self%source_dir /= "example" .or. pr > 2) then
+ write(unit, fmt) "- source directory", self%source_dir
+ end if
+ end if
+ if (allocated(self%main)) then
+ if (self%main /= "main.f90" .or. pr > 2) then
+ write(unit, fmt) "- example source", self%main
+ end if
+ end if
+
+ if (allocated(self%dependency)) then
+ if (size(self%dependency) > 1 .or. pr > 2) then
+ write(unit, fmti) "- dependencies", size(self%dependency)
+ end if
+ do ii = 1, size(self%dependency)
+ call self%dependency(ii)%info(unit, pr - 1)
+ end do
+ end if
+
+ end subroutine info
+
+
+end module fpm_manifest_example
diff --git a/src/fpm/manifest/executable.f90 b/src/fpm/manifest/executable.f90
new file mode 100644
index 0000000..be02974
--- /dev/null
+++ b/src/fpm/manifest/executable.f90
@@ -0,0 +1,186 @@
+!> Implementation of the meta data for an executables.
+!>
+!> An executable table can currently have the following fields
+!>
+!>```toml
+!>[[ executable ]]
+!>name = "string"
+!>source-dir = "path"
+!>main = "file"
+!>link = ["lib"]
+!>[executable.dependencies]
+!>```
+module fpm_manifest_executable
+ use fpm_manifest_dependency, only : dependency_config_t, new_dependencies
+ use fpm_error, only : error_t, syntax_error
+ use fpm_strings, only : string_t
+ use fpm_toml, only : toml_table, toml_key, toml_stat, get_value
+ implicit none
+ private
+
+ public :: executable_config_t, new_executable
+
+
+ !> Configuation meta data for an executable
+ type :: executable_config_t
+
+ !> Name of the resulting executable
+ character(len=:), allocatable :: name
+
+ !> Source directory for collecting the executable
+ character(len=:), allocatable :: source_dir
+
+ !> Name of the source file declaring the main program
+ character(len=:), allocatable :: main
+
+ !> Dependency meta data for this executable
+ type(dependency_config_t), allocatable :: dependency(:)
+
+ !> Libraries to link against
+ type(string_t), allocatable :: link(:)
+
+ contains
+
+ !> Print information on this instance
+ procedure :: info
+
+ end type executable_config_t
+
+
+contains
+
+
+ !> Construct a new executable configuration from a TOML data structure
+ subroutine new_executable(self, table, error)
+
+ !> Instance of the executable configuration
+ type(executable_config_t), intent(out) :: self
+
+ !> Instance of the TOML data structure
+ type(toml_table), intent(inout) :: table
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_table), pointer :: child
+
+ call check(table, error)
+ if (allocated(error)) return
+
+ call get_value(table, "name", self%name)
+ if (.not.allocated(self%name)) then
+ call syntax_error(error, "Could not retrieve executable name")
+ return
+ end if
+ call get_value(table, "source-dir", self%source_dir, "app")
+ call get_value(table, "main", self%main, "main.f90")
+
+ call get_value(table, "dependencies", child, requested=.false.)
+ if (associated(child)) then
+ call new_dependencies(self%dependency, child, error)
+ if (allocated(error)) return
+ end if
+
+ call get_value(table, "link", self%link, error)
+ if (allocated(error)) return
+
+ end subroutine new_executable
+
+
+ !> Check local schema for allowed entries
+ subroutine check(table, error)
+
+ !> Instance of the TOML data structure
+ type(toml_table), intent(inout) :: table
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_key), allocatable :: list(:)
+ logical :: name_present
+ integer :: ikey
+
+ name_present = .false.
+
+ call table%get_keys(list)
+
+ if (size(list) < 1) then
+ call syntax_error(error, "Executable section does not provide sufficient entries")
+ return
+ end if
+
+ do ikey = 1, size(list)
+ select case(list(ikey)%key)
+ case default
+ call syntax_error(error, "Key "//list(ikey)%key//" is not allowed as executable entry")
+ exit
+
+ case("name")
+ name_present = .true.
+
+ case("source-dir", "main", "dependencies", "link")
+ continue
+
+ end select
+ end do
+ if (allocated(error)) return
+
+ if (.not.name_present) then
+ call syntax_error(error, "Executable name is not provided, please add a name entry")
+ end if
+
+ end subroutine check
+
+
+ !> Write information on instance
+ subroutine info(self, unit, verbosity)
+
+ !> Instance of the executable configuration
+ class(executable_config_t), intent(in) :: self
+
+ !> Unit for IO
+ integer, intent(in) :: unit
+
+ !> Verbosity of the printout
+ integer, intent(in), optional :: verbosity
+
+ integer :: pr, ii
+ character(len=*), parameter :: fmt = '("#", 1x, a, t30, a)', &
+ & fmti = '("#", 1x, a, t30, i0)'
+
+ if (present(verbosity)) then
+ pr = verbosity
+ else
+ pr = 1
+ end if
+
+ if (pr < 1) return
+
+ write(unit, fmt) "Executable target"
+ if (allocated(self%name)) then
+ write(unit, fmt) "- name", self%name
+ end if
+ if (allocated(self%source_dir)) then
+ if (self%source_dir /= "app" .or. pr > 2) then
+ write(unit, fmt) "- source directory", self%source_dir
+ end if
+ end if
+ if (allocated(self%main)) then
+ if (self%main /= "main.f90" .or. pr > 2) then
+ write(unit, fmt) "- program source", self%main
+ end if
+ end if
+
+ if (allocated(self%dependency)) then
+ if (size(self%dependency) > 1 .or. pr > 2) then
+ write(unit, fmti) "- dependencies", size(self%dependency)
+ end if
+ do ii = 1, size(self%dependency)
+ call self%dependency(ii)%info(unit, pr - 1)
+ end do
+ end if
+
+ end subroutine info
+
+
+end module fpm_manifest_executable
diff --git a/src/fpm/manifest/install.f90 b/src/fpm/manifest/install.f90
new file mode 100644
index 0000000..6175873
--- /dev/null
+++ b/src/fpm/manifest/install.f90
@@ -0,0 +1,108 @@
+!> Implementation of the installation configuration.
+!>
+!> An install table can currently have the following fields
+!>
+!>```toml
+!>library = bool
+!>```
+module fpm_manifest_install
+ use fpm_error, only : error_t, fatal_error, syntax_error
+ use fpm_toml, only : toml_table, toml_key, toml_stat, get_value
+ implicit none
+ private
+
+ public :: install_config_t, new_install_config
+
+ !> Configuration data for installation
+ type :: install_config_t
+
+ !> Install library with this project
+ logical :: library
+
+ contains
+
+ !> Print information on this instance
+ procedure :: info
+
+ end type install_config_t
+
+contains
+
+ !> Create a new installation configuration from a TOML data structure
+ subroutine new_install_config(self, table, error)
+
+ !> Instance of the install configuration
+ type(install_config_t), intent(out) :: self
+
+ !> Instance of the TOML data structure
+ type(toml_table), intent(inout) :: table
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ call check(table, error)
+ if (allocated(error)) return
+
+ call get_value(table, "library", self%library, .false.)
+
+ end subroutine new_install_config
+
+
+ !> Check local schema for allowed entries
+ subroutine check(table, error)
+
+ !> Instance of the TOML data structure
+ type(toml_table), intent(inout) :: table
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_key), allocatable :: list(:)
+ integer :: ikey
+
+ call table%get_keys(list)
+ if (size(list) < 1) return
+
+ do ikey = 1, size(list)
+ select case(list(ikey)%key)
+ case default
+ call syntax_error(error, "Key "//list(ikey)%key//" is not allowed in install table")
+ exit
+ case("library")
+ continue
+ end select
+ end do
+ if (allocated(error)) return
+
+ end subroutine check
+
+ !> Write information on install configuration instance
+ subroutine info(self, unit, verbosity)
+
+ !> Instance of the build configuration
+ class(install_config_t), intent(in) :: self
+
+ !> Unit for IO
+ integer, intent(in) :: unit
+
+ !> Verbosity of the printout
+ integer, intent(in), optional :: verbosity
+
+ integer :: pr
+ character(len=*), parameter :: fmt = '("#", 1x, a, t30, a)'
+
+ if (present(verbosity)) then
+ pr = verbosity
+ else
+ pr = 1
+ end if
+
+ if (pr < 1) return
+
+ write(unit, fmt) "Install configuration"
+ write(unit, fmt) " - library install", &
+ & trim(merge("enabled ", "disabled", self%library))
+
+ end subroutine info
+
+end module fpm_manifest_install
diff --git a/src/fpm/manifest/library.f90 b/src/fpm/manifest/library.f90
new file mode 100644
index 0000000..c8ce049
--- /dev/null
+++ b/src/fpm/manifest/library.f90
@@ -0,0 +1,142 @@
+!> Implementation of the meta data for libraries.
+!>
+!> A library table can currently have the following fields
+!>
+!>```toml
+!>[library]
+!>source-dir = "path"
+!>include-dir = ["path1","path2"]
+!>build-script = "file"
+!>```
+module fpm_manifest_library
+ use fpm_error, only : error_t, syntax_error
+ use fpm_strings, only: string_t, string_cat
+ use fpm_toml, only : toml_table, toml_key, toml_stat, get_value
+ implicit none
+ private
+
+ public :: library_config_t, new_library
+
+
+ !> Configuration meta data for a library
+ type :: library_config_t
+
+ !> Source path prefix
+ character(len=:), allocatable :: source_dir
+
+ !> Include path prefix
+ type(string_t), allocatable :: include_dir(:)
+
+ !> Alternative build script to be invoked
+ character(len=:), allocatable :: build_script
+
+ contains
+
+ !> Print information on this instance
+ procedure :: info
+
+ end type library_config_t
+
+
+contains
+
+
+ !> Construct a new library configuration from a TOML data structure
+ subroutine new_library(self, table, error)
+
+ !> Instance of the library configuration
+ type(library_config_t), intent(out) :: self
+
+ !> Instance of the TOML data structure
+ type(toml_table), intent(inout) :: table
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ call check(table, error)
+ if (allocated(error)) return
+
+ call get_value(table, "source-dir", self%source_dir, "src")
+ call get_value(table, "build-script", self%build_script)
+
+ call get_value(table, "include-dir", self%include_dir, error)
+ if (allocated(error)) return
+
+ ! Set default value of include-dir if not found in manifest
+ if (.not.allocated(self%include_dir)) then
+ self%include_dir = [string_t("include")]
+ end if
+
+ end subroutine new_library
+
+
+ !> Check local schema for allowed entries
+ subroutine check(table, error)
+
+ !> Instance of the TOML data structure
+ type(toml_table), intent(inout) :: table
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_key), allocatable :: list(:)
+ integer :: ikey
+
+ call table%get_keys(list)
+
+ ! table can be empty
+ if (size(list) < 1) return
+
+ do ikey = 1, size(list)
+ select case(list(ikey)%key)
+ case default
+ call syntax_error(error, "Key "//list(ikey)%key//" is not allowed in library")
+ exit
+
+ case("source-dir", "include-dir", "build-script")
+ continue
+
+ end select
+ end do
+
+ end subroutine check
+
+
+ !> Write information on instance
+ subroutine info(self, unit, verbosity)
+
+ !> Instance of the library configuration
+ class(library_config_t), intent(in) :: self
+
+ !> Unit for IO
+ integer, intent(in) :: unit
+
+ !> Verbosity of the printout
+ integer, intent(in), optional :: verbosity
+
+ integer :: pr
+ character(len=*), parameter :: fmt = '("#", 1x, a, t30, a)'
+
+ if (present(verbosity)) then
+ pr = verbosity
+ else
+ pr = 1
+ end if
+
+ if (pr < 1) return
+
+ write(unit, fmt) "Library target"
+ if (allocated(self%source_dir)) then
+ write(unit, fmt) "- source directory", self%source_dir
+ end if
+ if (allocated(self%include_dir)) then
+ write(unit, fmt) "- include directory", string_cat(self%include_dir,",")
+ end if
+ if (allocated(self%build_script)) then
+ write(unit, fmt) "- custom build", self%build_script
+ end if
+
+ end subroutine info
+
+
+end module fpm_manifest_library
diff --git a/src/fpm/manifest/package.f90 b/src/fpm/manifest/package.f90
new file mode 100644
index 0000000..bbaa51d
--- /dev/null
+++ b/src/fpm/manifest/package.f90
@@ -0,0 +1,435 @@
+!> Define the package data containing the meta data from the configuration file.
+!>
+!> The package data defines a Fortran type corresponding to the respective
+!> TOML document, after creating it from a package file no more interaction
+!> with the TOML document is required.
+!>
+!> Every configuration type provides it custom constructor (prefixed with `new_`)
+!> and knows how to deserialize itself from a TOML document.
+!> To ensure we find no untracked content in the package file all keywords are
+!> checked and possible entries have to be explicitly allowed in the `check`
+!> function.
+!> If entries are mutally exclusive or interdependent inside the current table
+!> the `check` function is required to enforce this schema on the data structure.
+!>
+!> The package file root allows the following keywords
+!>
+!>```toml
+!>name = "string"
+!>version = "string"
+!>license = "string"
+!>author = "string"
+!>maintainer = "string"
+!>copyright = "string"
+!>[library]
+!>[dependencies]
+!>[dev-dependencies]
+!>[build]
+!>[install]
+!>[[ executable ]]
+!>[[ example ]]
+!>[[ test ]]
+!>```
+module fpm_manifest_package
+ use fpm_manifest_build, only: build_config_t, new_build_config
+ use fpm_manifest_dependency, only : dependency_config_t, new_dependencies
+ use fpm_manifest_example, only : example_config_t, new_example
+ use fpm_manifest_executable, only : executable_config_t, new_executable
+ use fpm_manifest_library, only : library_config_t, new_library
+ use fpm_manifest_install, only: install_config_t, new_install_config
+ use fpm_manifest_test, only : test_config_t, new_test
+ use fpm_error, only : error_t, fatal_error, syntax_error
+ use fpm_toml, only : toml_table, toml_array, toml_key, toml_stat, get_value, &
+ & len
+ use fpm_versioning, only : version_t, new_version
+ implicit none
+ private
+
+ public :: package_config_t, new_package
+
+
+ interface unique_programs
+ module procedure :: unique_programs1
+ module procedure :: unique_programs2
+ end interface unique_programs
+
+
+ !> Package meta data
+ type :: package_config_t
+
+ !> Name of the package
+ character(len=:), allocatable :: name
+
+ !> Package version
+ type(version_t) :: version
+
+ !> Build configuration data
+ type(build_config_t) :: build
+
+ !> Installation configuration data
+ type(install_config_t) :: install
+
+ !> Library meta data
+ type(library_config_t), allocatable :: library
+
+ !> Executable meta data
+ type(executable_config_t), allocatable :: executable(:)
+
+ !> Dependency meta data
+ type(dependency_config_t), allocatable :: dependency(:)
+
+ !> Development dependency meta data
+ type(dependency_config_t), allocatable :: dev_dependency(:)
+
+ !> Example meta data
+ type(example_config_t), allocatable :: example(:)
+
+ !> Test meta data
+ type(test_config_t), allocatable :: test(:)
+
+ contains
+
+ !> Print information on this instance
+ procedure :: info
+
+ end type package_config_t
+
+
+contains
+
+
+ !> Construct a new package configuration from a TOML data structure
+ subroutine new_package(self, table, error)
+
+ !> Instance of the package configuration
+ type(package_config_t), intent(out) :: self
+
+ !> Instance of the TOML data structure
+ type(toml_table), intent(inout) :: table
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ ! Backspace (8), tabulator (9), newline (10), formfeed (12) and carriage
+ ! return (13) are invalid in package names
+ character(len=*), parameter :: invalid_chars = &
+ achar(8) // achar(9) // achar(10) // achar(12) // achar(13)
+ type(toml_table), pointer :: child, node
+ type(toml_array), pointer :: children
+ character(len=:), allocatable :: version
+ integer :: ii, nn, stat
+
+ call check(table, error)
+ if (allocated(error)) return
+
+ call get_value(table, "name", self%name)
+ if (.not.allocated(self%name)) then
+ call syntax_error(error, "Could not retrieve package name")
+ return
+ end if
+
+ if (len(self%name) <= 0) then
+ call syntax_error(error, "Package name must be a non-empty string")
+ return
+ end if
+
+ ii = scan(self%name, invalid_chars)
+ if (ii > 0) then
+ call syntax_error(error, "Package name contains invalid characters")
+ return
+ end if
+
+ call get_value(table, "build", child, requested=.true., stat=stat)
+ if (stat /= toml_stat%success) then
+ call fatal_error(error, "Type mismatch for build entry, must be a table")
+ return
+ end if
+ call new_build_config(self%build, child, error)
+ if (allocated(error)) return
+
+ call get_value(table, "install", child, requested=.true., stat=stat)
+ if (stat /= toml_stat%success) then
+ call fatal_error(error, "Type mismatch for install entry, must be a table")
+ return
+ end if
+ call new_install_config(self%install, child, error)
+ if (allocated(error)) return
+
+ call get_value(table, "version", version, "0")
+ call new_version(self%version, version, error)
+ if (allocated(error)) return
+
+ call get_value(table, "dependencies", child, requested=.false.)
+ if (associated(child)) then
+ call new_dependencies(self%dependency, child, error)
+ if (allocated(error)) return
+ end if
+
+ call get_value(table, "dev-dependencies", child, requested=.false.)
+ if (associated(child)) then
+ call new_dependencies(self%dev_dependency, child, error)
+ if (allocated(error)) return
+ end if
+
+ call get_value(table, "library", child, requested=.false.)
+ if (associated(child)) then
+ allocate(self%library)
+ call new_library(self%library, child, error)
+ if (allocated(error)) return
+ end if
+
+ call get_value(table, "executable", children, requested=.false.)
+ if (associated(children)) then
+ nn = len(children)
+ allocate(self%executable(nn))
+ do ii = 1, nn
+ call get_value(children, ii, node, stat=stat)
+ if (stat /= toml_stat%success) then
+ call fatal_error(error, "Could not retrieve executable from array entry")
+ exit
+ end if
+ call new_executable(self%executable(ii), node, error)
+ if (allocated(error)) exit
+ end do
+ if (allocated(error)) return
+
+ call unique_programs(self%executable, error)
+ if (allocated(error)) return
+ end if
+
+ call get_value(table, "example", children, requested=.false.)
+ if (associated(children)) then
+ nn = len(children)
+ allocate(self%example(nn))
+ do ii = 1, nn
+ call get_value(children, ii, node, stat=stat)
+ if (stat /= toml_stat%success) then
+ call fatal_error(error, "Could not retrieve example from array entry")
+ exit
+ end if
+ call new_example(self%example(ii), node, error)
+ if (allocated(error)) exit
+ end do
+ if (allocated(error)) return
+
+ call unique_programs(self%example, error)
+ if (allocated(error)) return
+
+ if (allocated(self%executable)) then
+ call unique_programs(self%executable, self%example, error)
+ if (allocated(error)) return
+ end if
+ end if
+
+ call get_value(table, "test", children, requested=.false.)
+ if (associated(children)) then
+ nn = len(children)
+ allocate(self%test(nn))
+ do ii = 1, nn
+ call get_value(children, ii, node, stat=stat)
+ if (stat /= toml_stat%success) then
+ call fatal_error(error, "Could not retrieve test from array entry")
+ exit
+ end if
+ call new_test(self%test(ii), node, error)
+ if (allocated(error)) exit
+ end do
+ if (allocated(error)) return
+
+ call unique_programs(self%test, error)
+ if (allocated(error)) return
+ end if
+
+ end subroutine new_package
+
+
+ !> Check local schema for allowed entries
+ subroutine check(table, error)
+
+ !> Instance of the TOML data structure
+ type(toml_table), intent(inout) :: table
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_key), allocatable :: list(:)
+ logical :: name_present
+ integer :: ikey
+
+ name_present = .false.
+
+ call table%get_keys(list)
+
+ if (size(list) < 1) then
+ call syntax_error(error, "Package file is empty")
+ return
+ end if
+
+ do ikey = 1, size(list)
+ select case(list(ikey)%key)
+ case default
+ call syntax_error(error, "Key "//list(ikey)%key//" is not allowed in package file")
+ exit
+
+ case("name")
+ name_present = .true.
+
+ case("version", "license", "author", "maintainer", "copyright", &
+ & "description", "keywords", "categories", "homepage", "build", &
+ & "dependencies", "dev-dependencies", "test", "executable", &
+ & "example", "library", "install")
+ continue
+
+ end select
+ end do
+ if (allocated(error)) return
+
+ if (.not.name_present) then
+ call syntax_error(error, "Package name is not provided, please add a name entry")
+ end if
+
+ end subroutine check
+
+
+ !> Write information on instance
+ subroutine info(self, unit, verbosity)
+
+ !> Instance of the package configuration
+ class(package_config_t), intent(in) :: self
+
+ !> Unit for IO
+ integer, intent(in) :: unit
+
+ !> Verbosity of the printout
+ integer, intent(in), optional :: verbosity
+
+ integer :: pr, ii
+ character(len=*), parameter :: fmt = '("#", 1x, a, t30, a)', &
+ & fmti = '("#", 1x, a, t30, i0)'
+
+ if (present(verbosity)) then
+ pr = verbosity
+ else
+ pr = 1
+ end if
+
+ if (pr < 1) return
+
+ write(unit, fmt) "Package"
+ if (allocated(self%name)) then
+ write(unit, fmt) "- name", self%name
+ end if
+
+ call self%build%info(unit, pr - 1)
+
+ call self%install%info(unit, pr - 1)
+
+ if (allocated(self%library)) then
+ write(unit, fmt) "- target", "archive"
+ call self%library%info(unit, pr - 1)
+ end if
+
+ if (allocated(self%executable)) then
+ if (size(self%executable) > 1 .or. pr > 2) then
+ write(unit, fmti) "- executables", size(self%executable)
+ end if
+ do ii = 1, size(self%executable)
+ call self%executable(ii)%info(unit, pr - 1)
+ end do
+ end if
+
+ if (allocated(self%dependency)) then
+ if (size(self%dependency) > 1 .or. pr > 2) then
+ write(unit, fmti) "- dependencies", size(self%dependency)
+ end if
+ do ii = 1, size(self%dependency)
+ call self%dependency(ii)%info(unit, pr - 1)
+ end do
+ end if
+
+ if (allocated(self%example)) then
+ if (size(self%example) > 1 .or. pr > 2) then
+ write(unit, fmti) "- examples", size(self%example)
+ end if
+ do ii = 1, size(self%example)
+ call self%example(ii)%info(unit, pr - 1)
+ end do
+ end if
+
+ if (allocated(self%test)) then
+ if (size(self%test) > 1 .or. pr > 2) then
+ write(unit, fmti) "- tests", size(self%test)
+ end if
+ do ii = 1, size(self%test)
+ call self%test(ii)%info(unit, pr - 1)
+ end do
+ end if
+
+ if (allocated(self%dev_dependency)) then
+ if (size(self%dev_dependency) > 1 .or. pr > 2) then
+ write(unit, fmti) "- development deps.", size(self%dev_dependency)
+ end if
+ do ii = 1, size(self%dev_dependency)
+ call self%dev_dependency(ii)%info(unit, pr - 1)
+ end do
+ end if
+
+ end subroutine info
+
+
+ !> Check whether or not the names in a set of executables are unique
+ subroutine unique_programs1(executable, error)
+
+ !> Array of executables
+ class(executable_config_t), intent(in) :: executable(:)
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: i, j
+
+ do i = 1, size(executable)
+ do j = 1, i - 1
+ if (executable(i)%name == executable(j)%name) then
+ call fatal_error(error, "The program named '"//&
+ executable(j)%name//"' is duplicated. "//&
+ "Unique program names are required.")
+ exit
+ end if
+ end do
+ end do
+ if (allocated(error)) return
+
+ end subroutine unique_programs1
+
+
+ !> Check whether or not the names in a set of executables are unique
+ subroutine unique_programs2(executable_i, executable_j, error)
+
+ !> Array of executables
+ class(executable_config_t), intent(in) :: executable_i(:)
+
+ !> Array of executables
+ class(executable_config_t), intent(in) :: executable_j(:)
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: i, j
+
+ do i = 1, size(executable_i)
+ do j = 1, size(executable_j)
+ if (executable_i(i)%name == executable_j(j)%name) then
+ call fatal_error(error, "The program named '"//&
+ executable_j(j)%name//"' is duplicated. "//&
+ "Unique program names are required.")
+ exit
+ end if
+ end do
+ end do
+ if (allocated(error)) return
+
+ end subroutine unique_programs2
+
+
+end module fpm_manifest_package
diff --git a/src/fpm/manifest/test.f90 b/src/fpm/manifest/test.f90
new file mode 100644
index 0000000..bcacbd8
--- /dev/null
+++ b/src/fpm/manifest/test.f90
@@ -0,0 +1,175 @@
+!> Implementation of the meta data for a test.
+!>
+!> The test data structure is effectively a decorated version of an executable
+!> and shares most of its properties, except for the defaults and can be
+!> handled under most circumstances just like any other executable.
+!>
+!> A test table can currently have the following fields
+!>
+!>```toml
+!>[[ test ]]
+!>name = "string"
+!>source-dir = "path"
+!>main = "file"
+!>link = ["lib"]
+!>[test.dependencies]
+!>```
+module fpm_manifest_test
+ use fpm_manifest_dependency, only : dependency_config_t, new_dependencies
+ use fpm_manifest_executable, only : executable_config_t
+ use fpm_error, only : error_t, syntax_error
+ use fpm_toml, only : toml_table, toml_key, toml_stat, get_value
+ implicit none
+ private
+
+ public :: test_config_t, new_test
+
+
+ !> Configuation meta data for an test
+ type, extends(executable_config_t) :: test_config_t
+
+ contains
+
+ !> Print information on this instance
+ procedure :: info
+
+ end type test_config_t
+
+
+contains
+
+
+ !> Construct a new test configuration from a TOML data structure
+ subroutine new_test(self, table, error)
+
+ !> Instance of the test configuration
+ type(test_config_t), intent(out) :: self
+
+ !> Instance of the TOML data structure
+ type(toml_table), intent(inout) :: table
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_table), pointer :: child
+
+ call check(table, error)
+ if (allocated(error)) return
+
+ call get_value(table, "name", self%name)
+ if (.not.allocated(self%name)) then
+ call syntax_error(error, "Could not retrieve test name")
+ return
+ end if
+ call get_value(table, "source-dir", self%source_dir, "test")
+ call get_value(table, "main", self%main, "main.f90")
+
+ call get_value(table, "dependencies", child, requested=.false.)
+ if (associated(child)) then
+ call new_dependencies(self%dependency, child, error)
+ if (allocated(error)) return
+ end if
+
+ call get_value(table, "link", self%link, error)
+ if (allocated(error)) return
+
+ end subroutine new_test
+
+
+ !> Check local schema for allowed entries
+ subroutine check(table, error)
+
+ !> Instance of the TOML data structure
+ type(toml_table), intent(inout) :: table
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_key), allocatable :: list(:)
+ logical :: name_present
+ integer :: ikey
+
+ name_present = .false.
+
+ call table%get_keys(list)
+
+ if (size(list) < 1) then
+ call syntax_error(error, "Test section does not provide sufficient entries")
+ return
+ end if
+
+ do ikey = 1, size(list)
+ select case(list(ikey)%key)
+ case default
+ call syntax_error(error, "Key "//list(ikey)%key//" is not allowed in test entry")
+ exit
+
+ case("name")
+ name_present = .true.
+
+ case("source-dir", "main", "dependencies", "link")
+ continue
+
+ end select
+ end do
+ if (allocated(error)) return
+
+ if (.not.name_present) then
+ call syntax_error(error, "Test name is not provided, please add a name entry")
+ end if
+
+ end subroutine check
+
+
+ !> Write information on instance
+ subroutine info(self, unit, verbosity)
+
+ !> Instance of the test configuration
+ class(test_config_t), intent(in) :: self
+
+ !> Unit for IO
+ integer, intent(in) :: unit
+
+ !> Verbosity of the printout
+ integer, intent(in), optional :: verbosity
+
+ integer :: pr, ii
+ character(len=*), parameter :: fmt = '("#", 1x, a, t30, a)', &
+ & fmti = '("#", 1x, a, t30, i0)'
+
+ if (present(verbosity)) then
+ pr = verbosity
+ else
+ pr = 1
+ end if
+
+ if (pr < 1) return
+
+ write(unit, fmt) "Test target"
+ if (allocated(self%name)) then
+ write(unit, fmt) "- name", self%name
+ end if
+ if (allocated(self%source_dir)) then
+ if (self%source_dir /= "test" .or. pr > 2) then
+ write(unit, fmt) "- source directory", self%source_dir
+ end if
+ end if
+ if (allocated(self%main)) then
+ if (self%main /= "main.f90" .or. pr > 2) then
+ write(unit, fmt) "- test source", self%main
+ end if
+ end if
+
+ if (allocated(self%dependency)) then
+ if (size(self%dependency) > 1 .or. pr > 2) then
+ write(unit, fmti) "- dependencies", size(self%dependency)
+ end if
+ do ii = 1, size(self%dependency)
+ call self%dependency(ii)%info(unit, pr - 1)
+ end do
+ end if
+
+ end subroutine info
+
+
+end module fpm_manifest_test
diff --git a/src/fpm/toml.f90 b/src/fpm/toml.f90
new file mode 100644
index 0000000..dbaafcb
--- /dev/null
+++ b/src/fpm/toml.f90
@@ -0,0 +1,120 @@
+!># Interface to TOML processing library
+!>
+!> This module acts as a proxy to the `toml-f` public Fortran API and allows
+!> to selectively expose components from the library to `fpm`.
+!> The interaction with `toml-f` data types outside of this module should be
+!> limited to tables, arrays and key-lists, most of the necessary interactions
+!> are implemented in the building interface with the `get_value` and `set_value`
+!> procedures.
+!>
+!> This module allows to implement features necessary for `fpm`, which are
+!> not yet available in upstream `toml-f`.
+!>
+!> For more details on the library used see the
+!> [TOML-Fortran](https://toml-f.github.io/toml-f) developer pages.
+module fpm_toml
+ use fpm_error, only : error_t, fatal_error, file_not_found_error
+ use fpm_strings, only : string_t
+ use tomlf, only : toml_table, toml_array, toml_key, toml_stat, get_value, &
+ & set_value, toml_parse, toml_error, new_table, add_table, add_array, &
+ & toml_serializer, len
+ implicit none
+ private
+
+ public :: read_package_file
+ public :: toml_table, toml_array, toml_key, toml_stat, get_value, set_value
+ public :: new_table, add_table, add_array, len
+ public :: toml_error, toml_serializer, toml_parse
+
+
+ interface get_value
+ module procedure :: get_child_value_string_list
+ end interface get_value
+
+
+contains
+
+
+ !> Process the configuration file to a TOML data structure
+ subroutine read_package_file(table, manifest, error)
+
+ !> TOML data structure
+ type(toml_table), allocatable, intent(out) :: table
+
+ !> Name of the package configuration file
+ character(len=*), intent(in) :: manifest
+
+ !> Error status of the operation
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_error), allocatable :: parse_error
+ integer :: unit
+ logical :: exist
+
+ inquire(file=manifest, exist=exist)
+
+ if (.not.exist) then
+ call file_not_found_error(error, manifest)
+ return
+ end if
+
+ open(file=manifest, newunit=unit)
+ call toml_parse(table, unit, parse_error)
+ close(unit)
+
+ if (allocated(parse_error)) then
+ allocate(error)
+ call move_alloc(parse_error%message, error%message)
+ return
+ end if
+
+ end subroutine read_package_file
+
+
+ subroutine get_child_value_string_list(table, key, list, error)
+
+ !> Instance of the TOML data structure
+ type(toml_table), intent(inout) :: table
+
+ !> Key to read from
+ character(len=*), intent(in) :: key
+
+ !> List of strings to read
+ type(string_t), allocatable, intent(out) :: list(:)
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: stat, ilist, nlist
+ type(toml_array), pointer :: children
+ character(len=:), allocatable :: str
+
+ call get_value(table, key, children, requested=.false.)
+ if (associated(children)) then
+ nlist = len(children)
+ allocate(list(nlist))
+ do ilist = 1, nlist
+ call get_value(children, ilist, str, stat=stat)
+ if (stat /= toml_stat%success) then
+ call fatal_error(error, "Entry in "//key//" field cannot be read")
+ exit
+ end if
+ call move_alloc(str, list(ilist)%s)
+ end do
+ if (allocated(error)) return
+ else
+ call get_value(table, key, str, stat=stat)
+ if (stat /= toml_stat%success) then
+ call fatal_error(error, "Entry in "//key//" field cannot be read")
+ return
+ end if
+ if (allocated(str)) then
+ allocate(list(1))
+ call move_alloc(str, list(1)%s)
+ end if
+ end if
+
+ end subroutine get_child_value_string_list
+
+
+end module fpm_toml
diff --git a/src/fpm/versioning.f90 b/src/fpm/versioning.f90
new file mode 100644
index 0000000..b24fc3c
--- /dev/null
+++ b/src/fpm/versioning.f90
@@ -0,0 +1,412 @@
+!> Implementation of versioning data for comparing packages
+module fpm_versioning
+ use fpm_error, only : error_t, syntax_error
+ implicit none
+ private
+
+ public :: version_t, new_version, char
+
+
+ type :: version_t
+ private
+
+ !> Version numbers found
+ integer, allocatable :: num(:)
+
+ contains
+
+ generic :: operator(==) => equals
+ procedure, private :: equals
+
+ generic :: operator(/=) => not_equals
+ procedure, private :: not_equals
+
+ generic :: operator(>) => greater
+ procedure, private :: greater
+
+ generic :: operator(<) => less
+ procedure, private :: less
+
+ generic :: operator(>=) => greater_equals
+ procedure, private :: greater_equals
+
+ generic :: operator(<=) => less_equals
+ procedure, private :: less_equals
+
+ !> Compare a version against a version constraint (x.x.0 <= v < x.x.HUGE)
+ generic :: operator(.match.) => match
+ procedure, private :: match
+
+ !> Create a printable string from a version data type
+ procedure :: to_string
+
+ end type version_t
+
+
+ !> Arbitrary internal limit of the version parser
+ integer, parameter :: max_limit = 3
+
+
+ interface char
+ module procedure :: as_string
+ end interface char
+
+
+ interface new_version
+ module procedure :: new_version_from_string
+ module procedure :: new_version_from_int
+ end interface new_version
+
+
+contains
+
+
+ !> Create a new version from a string
+ subroutine new_version_from_int(self, num)
+
+ !> Instance of the versioning data
+ type(version_t), intent(out) :: self
+
+ !> Subversion numbers to define version data
+ integer, intent(in) :: num(:)
+
+ self%num = num
+
+ end subroutine new_version_from_int
+
+
+ !> Create a new version from a string
+ subroutine new_version_from_string(self, string, error)
+
+ !> Instance of the versioning data
+ type(version_t), intent(out) :: self
+
+ !> String describing the version information
+ character(len=*), intent(in) :: string
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ character :: tok
+ integer :: ii, istart, iend, stat, nn
+ integer :: num(max_limit)
+ logical :: is_number
+
+ nn = 0
+ iend = 0
+ istart = 0
+ is_number = .false.
+
+ do while(iend < len(string))
+ call next(string, istart, iend, is_number, error)
+ if (allocated(error)) exit
+ if (is_number) then
+ if (nn >= max_limit) then
+ call token_error(error, string, istart, iend, &
+ & "Too many subversions found")
+ exit
+ end if
+ nn = nn + 1
+ read(string(istart:iend), *, iostat=stat) num(nn)
+ if (stat /= 0) then
+ call token_error(error, string, istart, iend, &
+ & "Failed to parse version number")
+ exit
+ end if
+ end if
+ end do
+ if (allocated(error)) return
+ if (.not.is_number) then
+ call token_error(error, string, istart, iend, &
+ & "Expected version number, but no characters are left")
+ return
+ end if
+
+ call new_version(self, num(:nn))
+
+ end subroutine new_version_from_string
+
+
+ !> Tokenize a version string
+ subroutine next(string, istart, iend, is_number, error)
+
+ !> String describing the version information
+ character(len=*), intent(in) :: string
+
+ !> Start of last token, start of next token on exit
+ integer, intent(inout) :: istart
+
+ !> End of last token on entry, end of next token on exit
+ integer, intent(inout) :: iend
+
+ !> Token produced is a number
+ logical, intent(inout) :: is_number
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: ii, nn
+ logical :: was_number
+ character :: tok, last
+
+ was_number = is_number
+ nn = len(string)
+
+ if (iend >= nn) then
+ istart = nn
+ iend = nn
+ return
+ end if
+
+ ii = min(iend + 1, nn)
+ tok = string(ii:ii)
+
+ is_number = tok /= '.'
+ if (is_number .eqv. was_number) then
+ call token_error(error, string, istart, ii, &
+ & "Unexpected token found")
+ return
+ end if
+
+ if (.not.is_number) then
+ is_number = .false.
+ istart = ii
+ iend = ii
+ return
+ end if
+
+ istart = ii
+ do ii = min(iend + 1, nn), nn
+ tok = string(ii:ii)
+ select case(tok)
+ case default
+ call token_error(error, string, istart, ii, &
+ & "Invalid character in version number")
+ exit
+ case('.')
+ exit
+ case('0', '1', '2', '3', '4', '5', '6', '7', '8', '9')
+ iend = ii
+ cycle
+ end select
+ end do
+
+ end subroutine next
+
+
+ !> Create an error on an invalid token, provide some visual context as well
+ subroutine token_error(error, string, istart, iend, message)
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ !> String describing the version information
+ character(len=*), intent(in) :: string
+
+ !> Start of last token, start of next token on exit
+ integer, intent(in) :: istart
+
+ !> End of last token on entry, end of next token on exit
+ integer, intent(in) :: iend
+
+ !> Error message
+ character(len=*), intent(in) :: message
+
+ character(len=*), parameter :: nl = new_line('a')
+
+ allocate(error)
+ error%message = message // nl // " | " // string // nl // &
+ & " |" // repeat('-', istart) // repeat('^', iend - istart + 1)
+
+ end subroutine token_error
+
+
+ subroutine to_string(self, string)
+
+ !> Version number
+ class(version_t), intent(in) :: self
+
+ !> Character representation of the version
+ character(len=:), allocatable, intent(out) :: string
+
+ integer, parameter :: buffersize = 64
+ character(len=buffersize) :: buffer
+ integer :: ii
+
+ do ii = 1, size(self%num)
+ if (allocated(string)) then
+ write(buffer, '(".", i0)') self%num(ii)
+ string = string // trim(buffer)
+ else
+ write(buffer, '(i0)') self%num(ii)
+ string = trim(buffer)
+ end if
+ end do
+
+ if (.not.allocated(string)) then
+ string = '0'
+ end if
+
+ end subroutine to_string
+
+
+ function as_string(self) result(string)
+
+ !> Version number
+ class(version_t), intent(in) :: self
+
+ !> Character representation of the version
+ character(len=:), allocatable :: string
+
+ call self%to_string(string)
+
+ end function as_string
+
+
+ !> Check to version numbers for equality
+ elemental function equals(lhs, rhs) result(is_equal)
+
+ !> First version number
+ class(version_t), intent(in) :: lhs
+
+ !> Second version number
+ class(version_t), intent(in) :: rhs
+
+ !> Version match
+ logical :: is_equal
+
+ is_equal = .not.(lhs > rhs)
+ if (is_equal) then
+ is_equal = .not.(rhs > lhs)
+ end if
+
+ end function equals
+
+
+ !> Check two versions for inequality
+ elemental function not_equals(lhs, rhs) result(not_equal)
+
+ !> First version number
+ class(version_t), intent(in) :: lhs
+
+ !> Second version number
+ class(version_t), intent(in) :: rhs
+
+ !> Version mismatch
+ logical :: not_equal
+
+ not_equal = lhs > rhs
+ if (.not.not_equal) then
+ not_equal = rhs > lhs
+ end if
+
+ end function not_equals
+
+
+ !> Relative comparison of two versions
+ elemental function greater(lhs, rhs) result(is_greater)
+
+ !> First version number
+ class(version_t), intent(in) :: lhs
+
+ !> Second version number
+ class(version_t), intent(in) :: rhs
+
+ !> First version is greater
+ logical :: is_greater
+
+ integer :: ii
+
+ do ii = 1, min(size(lhs%num), size(rhs%num))
+ is_greater = lhs%num(ii) > rhs%num(ii)
+ if (is_greater) exit
+ end do
+ if (is_greater) return
+
+ is_greater = size(lhs%num) > size(rhs%num)
+ if (is_greater) then
+ do ii = size(rhs%num) + 1, size(lhs%num)
+ is_greater = lhs%num(ii) > 0
+ if (is_greater) exit
+ end do
+ end if
+
+ end function greater
+
+
+ !> Relative comparison of two versions
+ elemental function less(lhs, rhs) result(is_less)
+
+ !> First version number
+ class(version_t), intent(in) :: lhs
+
+ !> Second version number
+ class(version_t), intent(in) :: rhs
+
+ !> First version is less
+ logical :: is_less
+
+ is_less = rhs > lhs
+
+ end function less
+
+
+ !> Relative comparison of two versions
+ elemental function greater_equals(lhs, rhs) result(is_greater_equal)
+
+ !> First version number
+ class(version_t), intent(in) :: lhs
+
+ !> Second version number
+ class(version_t), intent(in) :: rhs
+
+ !> First version is greater or equal
+ logical :: is_greater_equal
+
+ is_greater_equal = .not. (rhs > lhs)
+
+ end function greater_equals
+
+
+ !> Relative comparison of two versions
+ elemental function less_equals(lhs, rhs) result(is_less_equal)
+
+ !> First version number
+ class(version_t), intent(in) :: lhs
+
+ !> Second version number
+ class(version_t), intent(in) :: rhs
+
+ !> First version is less or equal
+ logical :: is_less_equal
+
+ is_less_equal = .not. (lhs > rhs)
+
+ end function less_equals
+
+
+ !> Try to match first version against second version
+ elemental function match(lhs, rhs)
+
+ !> First version number
+ class(version_t), intent(in) :: lhs
+
+ !> Second version number
+ class(version_t), intent(in) :: rhs
+
+ !> Version match following semantic versioning rules
+ logical :: match
+
+ type(version_t) :: tmp
+
+ match = .not.(rhs > lhs)
+ if (match) then
+ tmp%num = rhs%num
+ tmp%num(size(tmp%num)) = tmp%num(size(tmp%num)) + 1
+ match = tmp > lhs
+ end if
+
+ end function match
+
+
+end module fpm_versioning
diff --git a/src/fpm_backend.f90 b/src/fpm_backend.f90
new file mode 100644
index 0000000..74cef61
--- /dev/null
+++ b/src/fpm_backend.f90
@@ -0,0 +1,262 @@
+!># Build backend
+!> Uses a list of `[[build_target_ptr]]` and a valid `[[fpm_model]]` instance
+!> to schedule and execute the compilation and linking of package targets.
+!>
+!> The package build process (`[[build_package]]`) comprises three steps:
+!>
+!> 1. __Target sorting:__ topological sort of the target dependency graph (`[[sort_target]]`)
+!> 2. __Target scheduling:__ group targets into schedule regions based on the sorting (`[[schedule_targets]]`)
+!> 3. __Target building:__ generate targets by compilation or linking
+!>
+!> @note If compiled with OpenMP, targets will be build in parallel where possible.
+!>
+!>### Incremental compilation
+!> The backend process supports *incremental* compilation whereby targets are not
+!> re-compiled if their corresponding dependencies have not been modified.
+!>
+!> - Source-based targets (*i.e.* objects) are not re-compiled if the corresponding source
+!> file is unmodified AND all of the target dependencies are not marked for re-compilation
+!>
+!> - Link targets (*i.e.* executables and libraries) are not re-compiled if the
+!> target output file already exists AND all of the target dependencies are not marked for
+!> re-compilation
+!>
+!> Source file modification is determined by a file digest (hash) which is calculated during
+!> the source parsing phase ([[fpm_source_parsing]]) and cached to disk after a target is
+!> successfully generated.
+!>
+module fpm_backend
+
+use fpm_environment, only: run
+use fpm_filesystem, only: dirname, join_path, exists, mkdir
+use fpm_model, only: fpm_model_t
+use fpm_targets, only: build_target_t, build_target_ptr, &
+ FPM_TARGET_OBJECT, FPM_TARGET_ARCHIVE, FPM_TARGET_EXECUTABLE
+
+use fpm_strings, only: string_cat
+
+implicit none
+
+private
+public :: build_package, sort_target, schedule_targets
+
+contains
+
+!> Top-level routine to build package described by `model`
+subroutine build_package(targets,model)
+ type(build_target_ptr), intent(inout) :: targets(:)
+ type(fpm_model_t), intent(in) :: model
+
+ integer :: i, j
+ type(build_target_ptr), allocatable :: queue(:)
+ integer, allocatable :: schedule_ptr(:)
+
+ ! Need to make output directory for include (mod) files
+ if (.not.exists(join_path(model%output_directory,model%package_name))) then
+ call mkdir(join_path(model%output_directory,model%package_name))
+ end if
+
+ ! Perform depth-first topological sort of targets
+ do i=1,size(targets)
+
+ call sort_target(targets(i)%ptr)
+
+ end do
+
+ ! Construct build schedule queue
+ call schedule_targets(queue, schedule_ptr, targets)
+
+ ! Loop over parallel schedule regions
+ do i=1,size(schedule_ptr)-1
+
+ ! Build targets in schedule region i
+ !$omp parallel do default(shared) schedule(dynamic,1)
+ do j=schedule_ptr(i),(schedule_ptr(i+1)-1)
+
+ call build_target(model,queue(j)%ptr)
+
+ end do
+
+ end do
+
+end subroutine build_package
+
+
+!> Topologically sort a target for scheduling by
+!> recursing over its dependencies.
+!>
+!> Checks disk-cached source hashes to determine if objects are
+!> up-to-date. Up-to-date sources are tagged as skipped.
+!>
+!> On completion, `target` should either be marked as
+!> sorted (`target%sorted=.true.`) or skipped (`target%skip=.true.`)
+!>
+!> If `target` is marked as sorted, `target%schedule` should be an
+!> integer greater than zero indicating the region for scheduling
+!>
+recursive subroutine sort_target(target)
+ type(build_target_t), intent(inout), target :: target
+
+ integer :: i, j, fh, stat
+ type(build_target_t), pointer :: exe_obj
+
+ ! Check if target has already been processed (as a dependency)
+ if (target%sorted .or. target%skip) then
+ return
+ end if
+
+ ! Check for a circular dependency
+ ! (If target has been touched but not processed)
+ if (target%touched) then
+ write(*,*) '(!) Circular dependency found with: ',target%output_file
+ stop
+ else
+ target%touched = .true. ! Set touched flag
+ end if
+
+ ! Load cached source file digest if present
+ if (.not.allocated(target%digest_cached) .and. &
+ exists(target%output_file) .and. &
+ exists(target%output_file//'.digest')) then
+
+ allocate(target%digest_cached)
+ open(newunit=fh,file=target%output_file//'.digest',status='old')
+ read(fh,*,iostat=stat) target%digest_cached
+ close(fh)
+
+ if (stat /= 0) then ! Cached digest is not recognized
+ deallocate(target%digest_cached)
+ end if
+
+ end if
+
+ if (allocated(target%source)) then
+
+ ! Skip if target is source-based and source file is unmodified
+ if (allocated(target%digest_cached)) then
+ if (target%digest_cached == target%source%digest) target%skip = .true.
+ end if
+
+ elseif (exists(target%output_file)) then
+
+ ! Skip if target is not source-based and already exists
+ target%skip = .true.
+
+ end if
+
+ ! Loop over target dependencies
+ target%schedule = 1
+ do i=1,size(target%dependencies)
+
+ ! Sort dependency
+ call sort_target(target%dependencies(i)%ptr)
+
+ if (.not.target%dependencies(i)%ptr%skip) then
+
+ ! Can't skip target if any dependency is not skipped
+ target%skip = .false.
+
+ ! Set target schedule after all of its dependencies
+ target%schedule = max(target%schedule,target%dependencies(i)%ptr%schedule+1)
+
+ end if
+
+ end do
+
+ ! Mark flag as processed: either sorted or skipped
+ target%sorted = .not.target%skip
+
+end subroutine sort_target
+
+
+!> Construct a build schedule from the sorted targets.
+!>
+!> The schedule is broken into regions, described by `schedule_ptr`,
+!> where targets in each region can be compiled in parallel.
+!>
+subroutine schedule_targets(queue, schedule_ptr, targets)
+ type(build_target_ptr), allocatable, intent(out) :: queue(:)
+ integer, allocatable :: schedule_ptr(:)
+ type(build_target_ptr), intent(in) :: targets(:)
+
+ integer :: i, j
+ integer :: n_schedule, n_sorted
+
+ n_schedule = 0 ! Number of schedule regions
+ n_sorted = 0 ! Total number of targets to build
+ do i=1,size(targets)
+
+ if (targets(i)%ptr%sorted) then
+ n_sorted = n_sorted + 1
+ end if
+ n_schedule = max(n_schedule, targets(i)%ptr%schedule)
+
+ end do
+
+ allocate(queue(n_sorted))
+ allocate(schedule_ptr(n_schedule+1))
+
+ ! Construct the target queue and schedule region pointer
+ n_sorted = 1
+ schedule_ptr(n_sorted) = 1
+ do i=1,n_schedule
+
+ do j=1,size(targets)
+
+ if (targets(j)%ptr%sorted) then
+ if (targets(j)%ptr%schedule == i) then
+
+ queue(n_sorted)%ptr => targets(j)%ptr
+ n_sorted = n_sorted + 1
+ end if
+ end if
+
+ end do
+
+ schedule_ptr(i+1) = n_sorted
+
+ end do
+
+end subroutine schedule_targets
+
+
+!> Call compile/link command for a single target.
+!>
+!> If successful, also caches the source file digest to disk.
+!>
+subroutine build_target(model,target)
+ type(fpm_model_t), intent(in) :: model
+ type(build_target_t), intent(in), target :: target
+
+ integer :: ilib, fh
+ character(:), allocatable :: link_flags
+
+ if (.not.exists(dirname(target%output_file))) then
+ call mkdir(dirname(target%output_file))
+ end if
+
+ select case(target%target_type)
+
+ case (FPM_TARGET_OBJECT)
+ call run(model%fortran_compiler//" -c " // target%source%file_name // target%compile_flags &
+ // " -o " // target%output_file)
+
+ case (FPM_TARGET_EXECUTABLE)
+
+ call run(model%fortran_compiler// " " // target%compile_flags &
+ //" "//target%link_flags// " -o " // target%output_file)
+
+ case (FPM_TARGET_ARCHIVE)
+ call run("ar -rs " // target%output_file // " " // string_cat(target%link_objects," "))
+
+ end select
+
+ if (allocated(target%source)) then
+ open(newunit=fh,file=target%output_file//'.digest',status='unknown')
+ write(fh,*) target%source%digest
+ close(fh)
+ end if
+
+end subroutine build_target
+
+end module fpm_backend
diff --git a/src/fpm_command_line.f90 b/src/fpm_command_line.f90
new file mode 100644
index 0000000..9e9a572
--- /dev/null
+++ b/src/fpm_command_line.f90
@@ -0,0 +1,1140 @@
+!># Definition of the command line interface
+!>
+!> This module uses [M_CLI2](https://github.com/urbanjost/M_CLI2) to define
+!> the command line interface.
+!> To define a command line interface create a new command settings type
+!> from the [[fpm_cmd_settings]] base class or the respective parent command
+!> settings.
+!>
+!> The subcommand is selected by the first non-option argument in the command
+!> line. In the subcase block the actual command line is defined and transferred
+!> to an instance of the [[fpm_cmd_settings]], the actual type is used by the
+!> *fpm* main program to determine which command entry point is chosen.
+!>
+!> To add a new subcommand add a new case to select construct and specify the
+!> wanted command line and the expected default values.
+!> Some of the following points also apply if you add a new option or argument
+!> to an existing *fpm* subcommand.
+!> At this point you should create a help page for the new command in a simple
+!> catman-like format as well in the ``set_help`` procedure.
+!> Make sure to register new subcommands in the ``fpm-manual`` command by adding
+!> them to the manual character array and in the help/manual case as well.
+!> You should add the new command to the synopsis section of the ``fpm-list``,
+!> ``fpm-help`` and ``fpm --list`` help pages below to make sure the help output
+!> is complete and consistent as well.
+module fpm_command_line
+use fpm_environment, only : get_os_type, get_env, &
+ OS_UNKNOWN, OS_LINUX, OS_MACOS, OS_WINDOWS, &
+ OS_CYGWIN, OS_SOLARIS, OS_FREEBSD
+use M_CLI2, only : set_args, lget, sget, unnamed, remaining, specified
+use fpm_strings, only : lower, split, fnv_1a
+use fpm_filesystem, only : basename, canon_path, to_fortran_name
+use fpm_compiler, only : get_default_compile_flags
+use,intrinsic :: iso_fortran_env, only : stdin=>input_unit, &
+ & stdout=>output_unit, &
+ & stderr=>error_unit
+implicit none
+
+private
+public :: fpm_cmd_settings, &
+ fpm_build_settings, &
+ fpm_install_settings, &
+ fpm_new_settings, &
+ fpm_run_settings, &
+ fpm_test_settings, &
+ fpm_update_settings, &
+ get_command_line_settings
+
+type, abstract :: fpm_cmd_settings
+ logical :: verbose=.true.
+end type
+
+integer,parameter :: ibug=4096
+type, extends(fpm_cmd_settings) :: fpm_new_settings
+ character(len=:),allocatable :: name
+ logical :: with_executable=.false.
+ logical :: with_test=.false.
+ logical :: with_lib=.true.
+ logical :: with_example=.false.
+ logical :: with_full=.false.
+ logical :: with_bare=.false.
+ logical :: backfill=.true.
+end type
+
+type, extends(fpm_cmd_settings) :: fpm_build_settings
+ logical :: list=.false.
+ logical :: show_model=.false.
+ character(len=:),allocatable :: compiler
+ character(len=:),allocatable :: profile
+ character(len=:),allocatable :: build_name
+ character(len=:),allocatable :: flag
+end type
+
+type, extends(fpm_build_settings) :: fpm_run_settings
+ character(len=ibug),allocatable :: name(:)
+ character(len=:),allocatable :: args
+ character(len=:),allocatable :: runner
+ logical :: example
+end type
+
+type, extends(fpm_run_settings) :: fpm_test_settings
+end type
+
+type, extends(fpm_build_settings) :: fpm_install_settings
+ character(len=:), allocatable :: prefix
+ character(len=:), allocatable :: bindir
+ character(len=:), allocatable :: libdir
+ character(len=:), allocatable :: includedir
+ logical :: no_rebuild
+end type
+
+!> Settings for interacting and updating with project dependencies
+type, extends(fpm_cmd_settings) :: fpm_update_settings
+ character(len=ibug),allocatable :: name(:)
+ logical :: fetch_only
+ logical :: clean
+end type
+
+character(len=:),allocatable :: name
+character(len=:),allocatable :: os_type
+character(len=ibug),allocatable :: names(:)
+character(len=:),allocatable :: tnames(:)
+
+character(len=:), allocatable :: version_text(:)
+character(len=:), allocatable :: help_new(:), help_fpm(:), help_run(:), &
+ & help_test(:), help_build(:), help_usage(:), help_runner(:), &
+ & help_text(:), help_install(:), help_help(:), help_update(:), &
+ & help_list(:), help_list_dash(:), help_list_nodash(:)
+character(len=20),parameter :: manual(*)=[ character(len=20) ::&
+& ' ', 'fpm', 'new', 'build', 'run', &
+& 'test', 'runner', 'install', 'update', 'list', 'help', 'version' ]
+
+character(len=:), allocatable :: val_runner, val_build, val_compiler, val_flag, val_profile
+
+contains
+ subroutine get_command_line_settings(cmd_settings)
+ class(fpm_cmd_settings), allocatable, intent(out) :: cmd_settings
+
+ character(len=4096) :: cmdarg
+ integer :: i
+ integer :: widest
+ type(fpm_install_settings), allocatable :: install_settings
+
+ call set_help()
+ ! text for --version switch,
+ select case (get_os_type())
+ case (OS_LINUX); os_type = "OS Type: Linux"
+ case (OS_MACOS); os_type = "OS Type: macOS"
+ case (OS_WINDOWS); os_type = "OS Type: Windows"
+ case (OS_CYGWIN); os_type = "OS Type: Cygwin"
+ case (OS_SOLARIS); os_type = "OS Type: Solaris"
+ case (OS_FREEBSD); os_type = "OS Type: FreeBSD"
+ case (OS_UNKNOWN); os_type = "OS Type: Unknown"
+ case default ; os_type = "OS Type: UNKNOWN"
+ end select
+ version_text = [character(len=80) :: &
+ & 'Version: 0.2.0, alpha', &
+ & 'Program: fpm(1)', &
+ & 'Description: A Fortran package manager and build system', &
+ & 'Home Page: https://github.com/fortran-lang/fpm', &
+ & 'License: MIT', &
+ & os_type]
+ ! find the subcommand name by looking for first word on command
+ ! not starting with dash
+ cmdarg=' '
+ do i = 1, command_argument_count()
+ call get_command_argument(i, cmdarg)
+ if(adjustl(cmdarg(1:1)) .ne. '-')exit
+ enddo
+
+ ! now set subcommand-specific help text and process commandline
+ ! arguments. Then call subcommand routine
+ select case(trim(cmdarg))
+
+ case('run')
+ call set_args('&
+ & --target " " &
+ & --list F &
+ & --all F &
+ & --profile " "&
+ & --example F&
+ & --runner " " &
+ & --compiler "'//get_env('FPM_COMPILER','gfortran')//'" &
+ & --flag:: " "&
+ & --verbose F&
+ & --',help_run,version_text)
+
+ call check_build_vals()
+
+ if( size(unnamed) .gt. 1 )then
+ names=unnamed(2:)
+ else
+ names=[character(len=len(names)) :: ]
+ endif
+
+
+ if(specified('target') )then
+ call split(sget('target'),tnames,delimiters=' ,:')
+ names=[character(len=max(len(names),len(tnames))) :: names,tnames]
+ endif
+
+ ! convert --all to '*'
+ if(lget('all'))then
+ names=[character(len=max(len(names),1)) :: names,'*' ]
+ endif
+
+ ! convert special string '..' to equivalent (shorter) '*'
+ ! to allow for a string that does not require shift-key and quoting
+ do i=1,size(names)
+ if(names(i).eq.'..')names(i)='*'
+ enddo
+
+ allocate(fpm_run_settings :: cmd_settings)
+ val_runner=sget('runner')
+ if(specified('runner') .and. val_runner.eq.'')val_runner='echo'
+ cmd_settings=fpm_run_settings(&
+ & args=remaining,&
+ & build_name=val_build,&
+ & profile=val_profile,&
+ & compiler=val_compiler, &
+ & flag=val_flag, &
+ & example=lget('example'), &
+ & list=lget('list'),&
+ & name=names,&
+ & runner=val_runner,&
+ & verbose=lget('verbose') )
+
+ case('build')
+ call set_args( '&
+ & --profile " " &
+ & --list F &
+ & --show-model F &
+ & --compiler "'//get_env('FPM_COMPILER','gfortran')//'" &
+ & --flag:: " "&
+ & --verbose F&
+ & --',help_build,version_text)
+
+ call check_build_vals()
+
+ allocate( fpm_build_settings :: cmd_settings )
+ cmd_settings=fpm_build_settings( &
+ & build_name=val_build,&
+ & profile=val_profile,&
+ & compiler=val_compiler, &
+ & flag=val_flag, &
+ & list=lget('list'),&
+ & show_model=lget('show-model'),&
+ & verbose=lget('verbose') )
+
+ case('new')
+ call set_args('&
+ & --src F &
+ & --lib F &
+ & --app F &
+ & --test F &
+ & --example F &
+ & --backfill F &
+ & --full F &
+ & --bare F &
+ & --verbose:V F',&
+ & help_new, version_text)
+ select case(size(unnamed))
+ case(1)
+ write(stderr,'(*(g0,/))')'<ERROR> directory name required'
+ write(stderr,'(*(7x,g0,/))') &
+ & '<USAGE> fpm new NAME [[--lib|--src] [--app] [--test] [--example]]|[--full|--bare] [--backfill]'
+ stop 1
+ case(2)
+ name=trim(unnamed(2))
+ case default
+ write(stderr,'(g0)')'<ERROR> only one directory name allowed'
+ write(stderr,'(7x,g0)') &
+ & '<USAGE> fpm new NAME [[--lib|--src] [--app] [--test] [--example]]| [--full|--bare] [--backfill]'
+ stop 2
+ end select
+ !*! canon_path is not converting ".", etc.
+ name=canon_path(name)
+ if( .not.is_fortran_name(to_fortran_name(basename(name))) )then
+ write(stderr,'(g0)') [ character(len=72) :: &
+ & '<ERROR> the fpm project name must be made of up to 63 ASCII letters,', &
+ & ' numbers, underscores, or hyphens, and start with a letter.']
+ stop 4
+ endif
+
+ allocate(fpm_new_settings :: cmd_settings)
+ if (any( specified([character(len=10) :: 'src','lib','app','test','example','bare'])) &
+ & .and.lget('full') )then
+ write(stderr,'(*(a))')&
+ &'<ERROR> --full and any of [--src|--lib,--app,--test,--example,--bare]', &
+ &' are mutually exclusive.'
+ stop 5
+ elseif (any( specified([character(len=10) :: 'src','lib','app','test','example','full'])) &
+ & .and.lget('bare') )then
+ write(stderr,'(*(a))')&
+ &'<ERROR> --bare and any of [--src|--lib,--app,--test,--example,--full]', &
+ &' are mutually exclusive.'
+ stop 3
+ elseif (any( specified([character(len=10) :: 'src','lib','app','test','example']) ) )then
+ cmd_settings=fpm_new_settings(&
+ & backfill=lget('backfill'), &
+ & name=name, &
+ & with_executable=lget('app'), &
+ & with_lib=any([lget('lib'),lget('src')]), &
+ & with_test=lget('test'), &
+ & with_example=lget('example'), &
+ & verbose=lget('verbose') )
+ else ! default if no specific directories are requested
+ cmd_settings=fpm_new_settings(&
+ & backfill=lget('backfill') , &
+ & name=name, &
+ & with_executable=.true., &
+ & with_lib=.true., &
+ & with_test=.true., &
+ & with_example=lget('full'), &
+ & with_full=lget('full'), &
+ & with_bare=lget('bare'), &
+ & verbose=lget('verbose') )
+ endif
+
+ case('help','manual')
+ call set_args('&
+ & --verbose F &
+ & ',help_help,version_text)
+ if(size(unnamed).lt.2)then
+ if(unnamed(1).eq.'help')then
+ unnamed=[' ', 'fpm']
+ else
+ unnamed=manual
+ endif
+ elseif(unnamed(2).eq.'manual')then
+ unnamed=manual
+ endif
+ widest=256
+ allocate(character(len=widest) :: help_text(0))
+ do i=2,size(unnamed)
+ select case(unnamed(i))
+ case(' ' )
+ case('fpm ' )
+ help_text=[character(len=widest) :: help_text, help_fpm]
+ case('new ' )
+ help_text=[character(len=widest) :: help_text, help_new]
+ case('build ' )
+ help_text=[character(len=widest) :: help_text, help_build]
+ case('install' )
+ help_text=[character(len=widest) :: help_text, help_install]
+ case('run ' )
+ help_text=[character(len=widest) :: help_text, help_run]
+ case('test ' )
+ help_text=[character(len=widest) :: help_text, help_test]
+ case('runner' )
+ help_text=[character(len=widest) :: help_text, help_runner]
+ case('list ' )
+ help_text=[character(len=widest) :: help_text, help_list]
+ case('update ' )
+ help_text=[character(len=widest) :: help_text, help_update]
+ case('help ' )
+ help_text=[character(len=widest) :: help_text, help_help]
+ case('version' )
+ help_text=[character(len=widest) :: help_text, version_text]
+ case default
+ help_text=[character(len=widest) :: help_text, &
+ & '<ERROR> unknown help topic "'//trim(unnamed(i))//'"']
+ !!& '<ERROR> unknown help topic "'//trim(unnamed(i)).'not found in:',manual]
+ end select
+ enddo
+ call printhelp(help_text)
+
+ case('install')
+ call set_args('--profile " " --no-rebuild F --verbose F --prefix " " &
+ & --list F &
+ & --compiler "'//get_env('FPM_COMPILER','gfortran')//'" &
+ & --flag:: " "&
+ & --libdir "lib" --bindir "bin" --includedir "include"', &
+ help_install, version_text)
+
+ call check_build_vals()
+
+ allocate(install_settings)
+ install_settings = fpm_install_settings(&
+ list=lget('list'), &
+ build_name=val_build, &
+ profile=val_profile,&
+ compiler=val_compiler, &
+ flag=val_flag, &
+ no_rebuild=lget('no-rebuild'), &
+ verbose=lget('verbose'))
+ call get_char_arg(install_settings%prefix, 'prefix')
+ call get_char_arg(install_settings%libdir, 'libdir')
+ call get_char_arg(install_settings%bindir, 'bindir')
+ call get_char_arg(install_settings%includedir, 'includedir')
+ call move_alloc(install_settings, cmd_settings)
+
+ case('list')
+ call set_args('&
+ & --list F&
+ & --verbose F&
+ &', help_list, version_text)
+ call printhelp(help_list_nodash)
+ if(lget('list'))then
+ call printhelp(help_list_dash)
+ endif
+ case('test')
+ call set_args('&
+ & --target " " &
+ & --list F&
+ & --profile " "&
+ & --runner " " &
+ & --compiler "'//get_env('FPM_COMPILER','gfortran')//'" &
+ & --flag:: " "&
+ & --verbose F&
+ & --',help_test,version_text)
+
+ call check_build_vals()
+
+ if( size(unnamed) .gt. 1 )then
+ names=unnamed(2:)
+ else
+ names=[character(len=len(names)) :: ]
+ endif
+
+ if(specified('target') )then
+ call split(sget('target'),tnames,delimiters=' ,:')
+ names=[character(len=max(len(names),len(tnames))) :: names,tnames]
+ endif
+
+ ! convert special string '..' to equivalent (shorter) '*'
+ ! to allow for a string that does not require shift-key and quoting
+ do i=1,size(names)
+ if(names(i).eq.'..')names(i)='*'
+ enddo
+
+ allocate(fpm_test_settings :: cmd_settings)
+ val_runner=sget('runner')
+ if(specified('runner') .and. val_runner.eq.'')val_runner='echo'
+ cmd_settings=fpm_test_settings(&
+ & args=remaining, &
+ & build_name=val_build, &
+ & profile=val_profile, &
+ & compiler=val_compiler, &
+ & flag=val_flag, &
+ & example=.false., &
+ & list=lget('list'), &
+ & name=names, &
+ & runner=val_runner, &
+ & verbose=lget('verbose') )
+
+ case('update')
+ call set_args('--fetch-only F --verbose F --clean F', &
+ help_update, version_text)
+
+ if( size(unnamed) .gt. 1 )then
+ names=unnamed(2:)
+ else
+ names=[character(len=len(names)) :: ]
+ endif
+
+ allocate(fpm_update_settings :: cmd_settings)
+ cmd_settings=fpm_update_settings(name=names, &
+ fetch_only=lget('fetch-only'), verbose=lget('verbose'), &
+ clean=lget('clean'))
+
+ case default
+
+ call set_args('&
+ & --list F&
+ & --verbose F&
+ &', help_fpm, version_text)
+ ! Note: will not get here if --version or --usage or --help
+ ! is present on commandline
+ help_text=help_usage
+ if(lget('list'))then
+ help_text=help_list_dash
+ elseif(len_trim(cmdarg).eq.0)then
+ write(stdout,'(*(a))')'Fortran Package Manager:'
+ write(stdout,'(*(a))')' '
+ call printhelp(help_list_nodash)
+ else
+ write(stderr,'(*(a))')'<ERROR> unknown subcommand [', &
+ & trim(cmdarg), ']'
+ call printhelp(help_list_dash)
+ endif
+ call printhelp(help_text)
+
+ end select
+ contains
+
+ subroutine check_build_vals()
+ character(len=:), allocatable :: flags
+
+ val_compiler=sget('compiler')
+ if(val_compiler.eq.'') then
+ val_compiler='gfortran'
+ endif
+
+ val_flag = " " // sget('flag')
+ val_profile = sget('profile')
+ if (val_flag == '') then
+ call get_default_compile_flags(val_compiler, val_profile == "release", val_flag)
+ else
+ select case(val_profile)
+ case("release", "debug")
+ call get_default_compile_flags(val_compiler, val_profile == "release", flags)
+ val_flag = flags // val_flag
+ end select
+ end if
+ allocate(character(len=16) :: val_build)
+ write(val_build, '(z16.16)') fnv_1a(val_flag)
+
+ end subroutine check_build_vals
+
+ subroutine printhelp(lines)
+ character(len=:),intent(in),allocatable :: lines(:)
+ integer :: iii,ii
+ if(allocated(lines))then
+ ii=size(lines)
+ if(ii .gt. 0 .and. len(lines).gt. 0) then
+ write(stdout,'(g0)')(trim(lines(iii)), iii=1, ii)
+ else
+ write(stdout,'(a)')'<WARNING> *printhelp* output requested is empty'
+ endif
+ endif
+ end subroutine printhelp
+
+ end subroutine get_command_line_settings
+
+ function is_fortran_name(line) result (lout)
+ ! determine if a string is a valid Fortran name ignoring trailing spaces
+ ! (but not leading spaces)
+ character(len=*),parameter :: int='0123456789'
+ character(len=*),parameter :: lower='abcdefghijklmnopqrstuvwxyz'
+ character(len=*),parameter :: upper='ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+ character(len=*),parameter :: allowed=upper//lower//int//'_'
+ character(len=*),intent(in) :: line
+ character(len=:),allocatable :: name
+ logical :: lout
+ name=trim(line)
+ if(len(name).ne.0)then
+ lout = .true. &
+ & .and. verify(name(1:1), lower//upper) == 0 &
+ & .and. verify(name,allowed) == 0 &
+ & .and. len(name) <= 63
+ else
+ lout = .false.
+ endif
+ end function is_fortran_name
+
+ subroutine set_help()
+ help_list_nodash=[character(len=80) :: &
+ 'USAGE: fpm [ SUBCOMMAND [SUBCOMMAND_OPTIONS] ]|[--list|--help|--version]', &
+ ' where SUBCOMMAND is commonly new|build|run|test ', &
+ ' ', &
+ ' subcommand may be one of ', &
+ ' ', &
+ ' build Compile the package placing results in the "build" directory', &
+ ' help Display help ', &
+ ' list Display this list of subcommand descriptions ', &
+ ' new Create a new Fortran package directory with sample files ', &
+ ' run Run the local package application programs ', &
+ ' test Run the test programs ', &
+ ' update Update and manage project dependencies ', &
+ ' install Install project ', &
+ ' ', &
+ ' Enter "fpm --list" for a brief list of subcommand options. Enter ', &
+ ' "fpm --help" or "fpm SUBCOMMAND --help" for detailed descriptions. ', &
+ ' ']
+ help_list_dash = [character(len=80) :: &
+ ' ', &
+ ' build [--compiler COMPILER_NAME] [--profile PROF] [--flag FFLAGS] [--list] ', &
+ ' help [NAME(s)] ', &
+ ' new NAME [[--lib|--src] [--app] [--test] [--example]]| ', &
+ ' [--full|--bare][--backfill] ', &
+ ' update [NAME(s)] [--fetch-only] [--clean] [--verbose] ', &
+ ' list [--list] ', &
+ ' run [[--target] NAME(s) [--example] [--profile PROF] [--flag FFLAGS] [--all] ', &
+ ' [--runner "CMD"] [--compiler COMPILER_NAME] [--list] [-- ARGS] ', &
+ ' test [[--target] NAME(s)] [--profile PROF] [--flag FFLAGS] [--runner "CMD"] [--list]', &
+ ' [--compiler COMPILER_NAME] [-- ARGS] ', &
+ ' install [--profile PROF] [--flag FFLAGS] [--no-rebuild] [--prefix PATH] [options] ', &
+ ' ']
+ help_usage=[character(len=80) :: &
+ '' ]
+ help_runner=[character(len=80) :: &
+ 'NAME ', &
+ ' --runner(1) - a shared option for specifying an application to launch ', &
+ ' executables. ', &
+ ' ', &
+ 'SYNOPSIS ', &
+ ' fpm run|test --runner CMD ... -- SUFFIX_OPTIONS ', &
+ ' ', &
+ 'DESCRIPTION ', &
+ ' The --runner option allows specifying a program to launch ', &
+ ' executables selected via the fpm(1) subcommands "run" and "test". This ', &
+ ' gives easy recourse to utilities such as debuggers and other tools ', &
+ ' that wrap other executables. ', &
+ ' ', &
+ ' These external commands are not part of fpm(1) itself as they vary ', &
+ ' from platform to platform or require independent installation. ', &
+ ' ', &
+ 'OPTION ', &
+ ' --runner ''CMD'' quoted command used to launch the fpm(1) executables. ', &
+ ' Available for both the "run" and "test" subcommands. ', &
+ ' If the keyword is specified without a value the default command ', &
+ ' is "echo". ', &
+ ' -- SUFFIX_OPTIONS additional options to suffix the command CMD and executable ', &
+ ' file names with. ', &
+ 'EXAMPLES ', &
+ ' Use cases for ''fpm run|test --runner "CMD"'' include employing ', &
+ ' the following common GNU/Linux and Unix commands: ', &
+ ' ', &
+ ' INTERROGATE ', &
+ ' + nm - list symbols from object files ', &
+ ' + size - list section sizes and total size. ', &
+ ' + ldd - print shared object dependencies ', &
+ ' + ls - list directory contents ', &
+ ' + stat - display file or file system status ', &
+ ' + file - determine file type ', &
+ ' PERFORMANCE AND DEBUGGING ', &
+ ' + gdb - The GNU Debugger ', &
+ ' + valgrind - a suite of tools for debugging and profiling ', &
+ ' + time - time a simple command or give resource usage ', &
+ ' + timeout - run a command with a time limit ', &
+ ' COPY ', &
+ ' + install - copy files and set attributes ', &
+ ' + tar - an archiving utility ', &
+ ' ALTER ', &
+ ' + rm - remove files or directories ', &
+ ' + chmod - change permissions of a file ', &
+ ' + strip - remove unnecessary information from strippable files ', &
+ ' ', &
+ ' For example ', &
+ ' ', &
+ ' fpm test --runner gdb ', &
+ ' fpm run --runner "tar cvfz $HOME/bundle.tgz" ', &
+ ' fpm run --runner ldd ', &
+ ' fpm run --runner strip ', &
+ ' fpm run --runner ''cp -t /usr/local/bin'' ', &
+ ' ', &
+ ' # options after executable name can be specified after the -- option ', &
+ ' fpm --runner cp run -- /usr/local/bin/ ', &
+ ' # generates commands of the form "cp $FILENAME /usr/local/bin/" ', &
+ ' ', &
+ ' # bash(1) alias example: ', &
+ ' alias fpm-install=\ ', &
+ ' "fpm run --profile release --runner ''install -vbp -m 0711 -t ~/.local/bin''" ', &
+ ' fpm-install ', &
+ '' ]
+ help_fpm=[character(len=80) :: &
+ 'NAME ', &
+ ' fpm(1) - A Fortran package manager and build system ', &
+ ' ', &
+ 'SYNOPSIS ', &
+ ' fpm SUBCOMMAND [SUBCOMMAND_OPTIONS] ', &
+ ' ', &
+ ' fpm --help|--version|--list ', &
+ ' ', &
+ 'DESCRIPTION ', &
+ ' fpm(1) is a package manager that helps you create Fortran projects ', &
+ ' from source -- it automatically determines dependencies! ', &
+ ' ', &
+ ' Most significantly fpm(1) lets you draw upon other fpm(1) packages ', &
+ ' in distributed git(1) repositories as if the packages were a basic ', &
+ ' part of your default programming environment, as well as letting ', &
+ ' you share your projects with others in a similar manner. ', &
+ ' ', &
+ ' All output goes into the directory "build/" which can generally be ', &
+ ' removed and rebuilt if required. Note that if external packages are ', &
+ ' being used you need network connectivity to rebuild from scratch. ', &
+ ' ', &
+ 'SUBCOMMANDS ', &
+ ' Valid fpm(1) subcommands are: ', &
+ ' ', &
+ ' + build Compile the packages into the "build/" directory. ', &
+ ' + new Create a new Fortran package directory with sample files. ', &
+ ' + update Update the project dependencies. ', &
+ ' + run Run the local package binaries. defaults to all binaries for ', &
+ ' that release. ', &
+ ' + test Run the tests. ', &
+ ' + help Alternate method for displaying subcommand help. ', &
+ ' + list Display brief descriptions of all subcommands. ', &
+ ' + install Install project ', &
+ ' ', &
+ ' Their syntax is ', &
+ ' ', &
+ ' build [--profile PROF] [--flag FFLAGS] [--list] [--compiler COMPILER_NAME]', &
+ ' new NAME [[--lib|--src] [--app] [--test] [--example]]| ', &
+ ' [--full|--bare][--backfill] ', &
+ ' update [NAME(s)] [--fetch-only] [--clean] ', &
+ ' run [[--target] NAME(s)] [--profile PROF] [--flag FFLAGS] [--list] [--example]', &
+ ' [--all] [--runner "CMD"] [--compiler COMPILER_NAME] [-- ARGS] ', &
+ ' test [[--target] NAME(s)] [--profile PROF] [--flag FFLAGS] [--list] ', &
+ ' [--runner "CMD"] [--compiler COMPILER_NAME] [-- ARGS] ', &
+ ' help [NAME(s)] ', &
+ ' list [--list] ', &
+ ' install [--profile PROF] [--flag FFLAGS] [--no-rebuild] [--prefix PATH] [options]', &
+ ' ', &
+ 'SUBCOMMAND OPTIONS ', &
+ ' --profile PROF selects the compilation profile for the build.',&
+ ' Currently available profiles are "release" for',&
+ ' high optimization and "debug" for full debug options.',&
+ ' If --flag is not specified the "debug" flags are the',&
+ ' default. ',&
+ ' --flag FFLAGS selects compile arguments for the build. These are',&
+ ' added to the profile options if --profile is specified,',&
+ ' else these options override the defaults.',&
+ ' Note object and .mod directory locations are always',&
+ ' built in.',&
+ ' --list List candidates instead of building or running them. On ', &
+ ' the fpm(1) command this shows a brief list of subcommands.', &
+ ' --runner CMD Provides a command to prefix program execution paths. ', &
+ ' --compiler COMPILER_NAME Compiler name. The environment variable ', &
+ ' FPM_COMPILER sets the default. ', &
+ ' -- ARGS Arguments to pass to executables. ', &
+ ' ', &
+ 'VALID FOR ALL SUBCOMMANDS ', &
+ ' --help Show help text and exit ', &
+ ' --verbose Display additional information when available ', &
+ ' --version Show version information and exit. ', &
+ ' ', &
+ 'EXAMPLES ', &
+ ' sample commands: ', &
+ ' ', &
+ ' fpm new mypackage --app --test ', &
+ ' fpm build ', &
+ ' fpm test ', &
+ ' fpm run ', &
+ ' fpm run --example ', &
+ ' fpm new --help ', &
+ ' fpm run myprogram --profile release -- -x 10 -y 20 --title "my title"', &
+ ' fpm install --prefix ~/.local ', &
+ ' ', &
+ 'SEE ALSO ', &
+ ' ', &
+ ' + The fpm(1) home page is at https://github.com/fortran-lang/fpm ', &
+ ' + Registered fpm(1) packages are at https://fortran-lang.org/packages ', &
+ ' + The fpm(1) TOML file format is described at ', &
+ ' https://github.com/fortran-lang/fpm/blob/master/manifest-reference.md ', &
+ '']
+ help_list=[character(len=80) :: &
+ 'NAME ', &
+ ' list(1) - list summary of fpm(1) subcommands ', &
+ ' ', &
+ 'SYNOPSIS ', &
+ ' fpm list [-list] ', &
+ ' ', &
+ ' fpm list --help|--version ', &
+ ' ', &
+ 'DESCRIPTION ', &
+ ' Display a short description for each fpm(1) subcommand. ', &
+ ' ', &
+ 'OPTIONS ', &
+ ' --list display a list of command options as well. This is the ', &
+ ' same output as generated by "fpm --list". ', &
+ ' ', &
+ 'EXAMPLES ', &
+ ' display a short list of fpm(1) subcommands ', &
+ ' ', &
+ ' fpm list ', &
+ ' fpm --list ', &
+ '' ]
+ help_run=[character(len=80) :: &
+ 'NAME ', &
+ ' run(1) - the fpm(1) subcommand to run project applications ', &
+ ' ', &
+ 'SYNOPSIS ', &
+ ' fpm run [[--target] NAME(s) [--profile PROF] [--flag FFLAGS]', &
+ ' [--compiler COMPILER_NAME] [--runner "CMD"] [--example]', &
+ ' [--list] [--all] [-- ARGS]', &
+ ' ', &
+ ' fpm run --help|--version ', &
+ ' ', &
+ 'DESCRIPTION ', &
+ ' Run the applications in your fpm(1) package. By default applications ', &
+ ' in /app or specified as "executable" in your "fpm.toml" manifest are ', &
+ ' used. Alternatively demonstration programs in example/ or specified in', &
+ ' the "example" section in "fpm.toml" can be executed. The applications ', &
+ ' are automatically rebuilt before being run if they are out of date. ', &
+ ' ', &
+ 'OPTIONS ', &
+ ' --target NAME(s) list of application names to execute. No name is ', &
+ ' required if only one target exists. If no name is ', &
+ ' supplied and more than one candidate exists or a ', &
+ ' name has no match a list is produced and fpm(1) ', &
+ ' exits. ', &
+ ' ', &
+ ' Basic "globbing" is supported where "?" represents ', &
+ ' any single character and "*" represents any string. ', &
+ ' Note The glob string normally needs quoted to ', &
+ ' the special characters from shell expansion. ', &
+ ' --all Run all examples or applications. An alias for --target ''*''. ', &
+ ' --example Run example programs instead of applications. ', &
+ ' --profile PROF selects the compilation profile for the build.',&
+ ' Currently available profiles are "release" for',&
+ ' high optimization and "debug" for full debug options.',&
+ ' If --flag is not specified the "debug" flags are the',&
+ ' default. ',&
+ ' --flag FFLAGS selects compile arguments for the build. These are',&
+ ' added to the profile options if --profile is specified,',&
+ ' else these options override the defaults.',&
+ ' Note object and .mod directory locations are always',&
+ ' built in.',&
+ ' --compiler COMPILER_NAME Specify a compiler name. The default is ', &
+ ' "gfortran" unless set by the environment ', &
+ ' variable FPM_COMPILER. ', &
+ ' --runner CMD A command to prefix the program execution paths with. ', &
+ ' see "fpm help runner" for further details. ', &
+ ' --list list pathname of candidates instead of running them. Note ', &
+ ' out-of-date candidates will still be rebuilt before being ', &
+ ' listed. ', &
+ ' -- ARGS optional arguments to pass to the program(s). The same ', &
+ ' arguments are passed to all program names specified. ', &
+ ' ', &
+ 'EXAMPLES ', &
+ ' fpm(1) - run or display project applications: ', &
+ ' ', &
+ ' fpm run # run a target when only one exists or list targets ', &
+ ' fpm run --list # list all targets, running nothing. ', &
+ ' fpm run --all # run all targets, no matter how many there are. ', &
+ ' ', &
+ ' # run default program built or to be built with the compiler command ', &
+ ' # "f90". If more than one app exists a list displays and target names', &
+ ' # are required. ', &
+ ' fpm run --compiler f90 ', &
+ ' ', &
+ ' # run example programs instead of the application programs. ', &
+ ' fpm run --example ''*'' ', &
+ ' ', &
+ ' # run a specific program and pass arguments to the command ', &
+ ' fpm run myprog -- -x 10 -y 20 --title "my title line" ', &
+ ' ', &
+ ' # run production version of two applications ', &
+ ' fpm run --target prg1,prg2 --profile release ', &
+ ' ', &
+ ' # install executables in directory (assuming install(1) exists) ', &
+ ' fpm run --runner ''install -b -m 0711 -p -t /usr/local/bin'' ', &
+ '' ]
+ help_build=[character(len=80) :: &
+ 'NAME ', &
+ ' build(1) - the fpm(1) subcommand to build a project ', &
+ ' ', &
+ 'SYNOPSIS ', &
+ ' fpm build [--profile PROF] [--flag FFLAGS] [--compiler COMPILER_NAME] [-list]', &
+ ' ', &
+ ' fpm build --help|--version ', &
+ ' ', &
+ 'DESCRIPTION ', &
+ ' The "fpm build" command ', &
+ ' o Fetches any dependencies ', &
+ ' o Scans your sources ', &
+ ' o Builds them in the proper order ', &
+ ' ', &
+ ' The Fortran source files are assumed by default to be in ', &
+ ' o src/ for modules and procedure source ', &
+ ' o app/ main program(s) for applications ', &
+ ' o test/ main program(s) and support files for project tests ', &
+ ' o example/ main program(s) for example programs ', &
+ ' Changed or new files found are rebuilt. The results are placed in ', &
+ ' the build/ directory. ', &
+ ' ', &
+ ' Non-default pathnames and remote dependencies are used if ', &
+ ' specified in the "fpm.toml" file. ', &
+ ' ', &
+ 'OPTIONS ', &
+ ' --profile PROF selects the compilation profile for the build.',&
+ ' Currently available profiles are "release" for',&
+ ' high optimization and "debug" for full debug options.',&
+ ' If --flag is not specified the "debug" flags are the',&
+ ' default. ',&
+ ' --flag FFLAGS selects compile arguments for the build. These are',&
+ ' added to the profile options if --profile is specified,',&
+ ' else these options override the defaults.',&
+ ' Note object and .mod directory locations are always',&
+ ' built in.',&
+ ' --compiler COMPILER_NAME Specify a compiler name. The default is ', &
+ ' "gfortran" unless set by the environment ', &
+ ' variable FPM_COMPILER. ', &
+ ' --list list candidates instead of building or running them ', &
+ ' --show-model show the model and exit (do not build) ', &
+ ' --help print this help and exit ', &
+ ' --version print program version information and exit ', &
+ ' ', &
+ 'EXAMPLES ', &
+ ' Sample commands: ', &
+ ' ', &
+ ' fpm build # build with debug options ', &
+ ' fpm build --profile release # build with high optimization ', &
+ '' ]
+
+ help_help=[character(len=80) :: &
+ 'NAME ', &
+ ' help(1) - the fpm(1) subcommand to display help ', &
+ ' ', &
+ 'SYNOPSIS ', &
+ ' fpm help [fpm] [new] [build] [run] [test] [help] [version] [manual] ', &
+ ' [runner] ', &
+ ' ', &
+ 'DESCRIPTION ', &
+ ' The "fpm help" command is an alternative to the --help parameter ', &
+ ' on the fpm(1) command and its subcommands. ', &
+ ' ', &
+ 'OPTIONS ', &
+ ' NAME(s) A list of topic names to display. All the subcommands ', &
+ ' have their own page (new, build, run, test, ...). ', &
+ ' ', &
+ ' The special name "manual" displays all the fpm(1) ', &
+ ' built-in documentation. ', &
+ ' ', &
+ ' The default is to display help for the fpm(1) command ', &
+ ' itself. ', &
+ ' ', &
+ 'EXAMPLES ', &
+ ' Sample usage: ', &
+ ' ', &
+ ' fpm help # general fpm(1) command help ', &
+ ' fpm help version # show program version ', &
+ ' fpm help new # display help for "new" subcommand ', &
+ ' fpm help manual # All fpm(1) built-in documentation ', &
+ ' ', &
+ '' ]
+ help_new=[character(len=80) :: &
+ 'NAME ', &
+ ' new(1) - the fpm(1) subcommand to initialize a new project ', &
+ 'SYNOPSIS ', &
+ ' fpm new NAME [[--lib|--src] [--app] [--test] [--example]]| ', &
+ ' [--full|--bare][--backfill] ', &
+ ' fpm new --help|--version ', &
+ ' ', &
+ 'DESCRIPTION ', &
+ ' "fpm new" creates and populates a new programming project directory. ', &
+ ' It ', &
+ ' o creates a directory with the specified name ', &
+ ' o runs the command "git init" in that directory ', &
+ ' o populates the directory with the default project directories ', &
+ ' o adds sample Fortran source files ', &
+ ' o adds a ".gitignore" file for ignoring the build/ directory ', &
+ ' (where fpm-generated output will be placed) ', &
+ ' ', &
+ ' The default file structure (that will be automatically scanned) is ', &
+ ' ', &
+ ' NAME/ ', &
+ ' fpm.toml ', &
+ ' .gitignore ', &
+ ' src/ ', &
+ ' NAME.f90 ', &
+ ' app/ ', &
+ ' main.f90 ', &
+ ' test/ ', &
+ ' check.f90 ', &
+ ' example/ ', &
+ ' demo.f90 ', &
+ ' ', &
+ ' Using this file structure is highly encouraged, particularly for ', &
+ ' small packages primarily intended to be used as dependencies. ', &
+ ' ', &
+ ' If you find this restrictive and need to customize the package ', &
+ ' structure you will find using the --full switch creates a ', &
+ ' heavily annotated manifest file with references to documentation ', &
+ ' to aid in constructing complex package structures. ', &
+ ' ', &
+ ' Remember to update the information in the sample "fpm.toml" ', &
+ ' file with your name and e-mail address. ', &
+ ' ', &
+ 'OPTIONS ', &
+ ' NAME the name of the project directory to create. The name ', &
+ ' must be made of up to 63 ASCII letters, digits, underscores, ', &
+ ' or hyphens, and start with a letter. ', &
+ ' ', &
+ ' The default is to create the src/, app/, and test/ directories. ', &
+ ' If any of the following options are specified then only the ', &
+ ' selected subdirectories are generated: ', &
+ ' ', &
+ ' --lib,--src create directory src/ and a placeholder module ', &
+ ' named "NAME.f90" for use with subcommand "build". ', &
+ ' --app create directory app/ and a placeholder main ', &
+ ' program for use with subcommand "run". ', &
+ ' --test create directory test/ and a placeholder program ', &
+ ' for use with the subcommand "test". Note that sans ', &
+ ' "--lib" it really does not have anything to test. ', &
+ ' --example create directory example/ and a placeholder program ', &
+ ' for use with the subcommand "run --example". ', &
+ ' It is only created by default if "--full is" specified. ', &
+ ' ', &
+ ' So the default is equivalent to ',&
+ ' ', &
+ ' fpm NAME --lib --app --test ', &
+ ' ', &
+ ' --backfill By default the directory must not exist. If this ', &
+ ' option is present the directory may pre-exist and ', &
+ ' only subdirectories and files that do not ', &
+ ' already exist will be created. For example, if you ', &
+ ' previously entered "fpm new myname --lib" entering ', &
+ ' "fpm new myname -full --backfill" will create any missing', &
+ ' app/, example/, and test/ directories and programs. ', &
+ ' ', &
+ ' --full By default a minimal manifest file ("fpm.toml") is ', &
+ ' created that depends on auto-discovery. With this ', &
+ ' option a much more extensive manifest sample is written ', &
+ ' and the example/ directory is created and populated. ', &
+ ' It is designed to facilitate creating projects that ', &
+ ' depend extensively on non-default build options. ', &
+ ' ', &
+ ' --bare A minimal manifest file ("fpm.toml") is created and ', &
+ ' a ".gitignore" and "README.md" file is created but no ', &
+ ' directories or sample Fortran is generated. ', &
+ ' ', &
+ ' --help print this help and exit ', &
+ ' --version print program version information and exit ', &
+ ' ', &
+ 'EXAMPLES ', &
+ ' Sample use ', &
+ ' ', &
+ ' fpm new myproject # create new project directory and seed it ', &
+ ' cd myproject # Enter the new directory ', &
+ ' # and run commands such as ', &
+ ' fpm build ', &
+ ' fpm run # run lone example application program ', &
+ ' fpm test # run example test program(s) ', &
+ ' fpm run --example # run lone example program ', &
+ ' ', &
+ ' fpm new A --full # create example/ and an annotated fpm.toml as well', &
+ ' fpm new A --bare # create no directories ', &
+ ' create any missing files in current directory ', &
+ ' fpm new `pwd` --full --backfill ', &
+ '' ]
+ help_test=[character(len=80) :: &
+ 'NAME ', &
+ ' test(1) - the fpm(1) subcommand to run project tests ', &
+ ' ', &
+ 'SYNOPSIS ', &
+ ' fpm test [[--target] NAME(s)] [--profile PROF] [--flag FFLAGS]', &
+ ' [--compiler COMPILER_NAME ] [--runner "CMD"] [--list][-- ARGS]', &
+ ' ', &
+ ' fpm test --help|--version ', &
+ ' ', &
+ 'DESCRIPTION ', &
+ ' Run applications you have built to test your project. ', &
+ ' ', &
+ 'OPTIONS ', &
+ ' --target NAME(s) optional list of specific test names to execute. ', &
+ ' The default is to run all the tests in test/ ', &
+ ' or the tests listed in the "fpm.toml" file. ', &
+ ' ', &
+ ' Basic "globbing" is supported where "?" represents ', &
+ ' any single character and "*" represents any string. ', &
+ ' Note The glob string normally needs quoted to ', &
+ ' protect the special characters from shell expansion.', &
+ ' --profile PROF selects the compilation profile for the build.',&
+ ' Currently available profiles are "release" for',&
+ ' high optimization and "debug" for full debug options.',&
+ ' If --flag is not specified the "debug" flags are the',&
+ ' default. ',&
+ ' --flag FFLAGS selects compile arguments for the build. These are',&
+ ' added to the profile options if --profile is specified,',&
+ ' else these options override the defaults.',&
+ ' Note object and .mod directory locations are always',&
+ ' built in.',&
+ ' --compiler COMPILER_NAME Specify a compiler name. The default is ', &
+ ' "gfortran" unless set by the environment ', &
+ ' variable FPM_COMPILER. ', &
+ ' --runner CMD A command to prefix the program execution paths with. ', &
+ ' see "fpm help runner" for further details. ', &
+ ' --list list candidates instead of building or running them ', &
+ ' -- ARGS optional arguments to pass to the test program(s). ', &
+ ' The same arguments are passed to all test names ', &
+ ' specified. ', &
+ ' ', &
+ 'EXAMPLES ', &
+ 'run tests ', &
+ ' ', &
+ ' # run default tests in /test or as specified in "fpm.toml" ', &
+ ' fpm test ', &
+ ' ', &
+ ' # run using compiler command "f90" ', &
+ ' fpm test --compiler f90 ', &
+ ' ', &
+ ' # run a specific test and pass arguments to the command ', &
+ ' fpm test mytest -- -x 10 -y 20 --title "my title line" ', &
+ ' ', &
+ ' fpm test tst1 tst2 --profile PROF # run production version of two tests', &
+ '' ]
+ help_update=[character(len=80) :: &
+ 'NAME', &
+ ' update(1) - manage project dependencies', &
+ '', &
+ 'SYNOPSIS', &
+ ' fpm update [--fetch-only] [--clean] [--verbose] [NAME(s)]', &
+ '', &
+ 'DESCRIPTION', &
+ ' Manage and update project dependencies. If no dependency names are', &
+ ' provided all the dependencies are updated automatically.', &
+ '', &
+ 'OPTIONS', &
+ ' --fetch-only Only fetch dependencies, do not update existing projects', &
+ ' --clean Do not use previous dependency cache', &
+ ' --verbose Show additional printout', &
+ '', &
+ 'SEE ALSO', &
+ ' The fpm(1) home page at https://github.com/fortran-lang/fpm', &
+ '' ]
+ help_install=[character(len=80) :: &
+ 'NAME', &
+ ' install(1) - install fpm projects', &
+ '', &
+ 'SYNOPSIS', &
+ ' fpm install [--profile PROF] [--flag FFLAGS] [--list] [--no-rebuild]', &
+ ' [--prefix DIR] [--bindir DIR] [--libdir DIR] [--includedir DIR]', &
+ ' [--verbose]', &
+ '', &
+ 'DESCRIPTION', &
+ ' Subcommand to install fpm projects. Running install will export the', &
+ ' current project to the selected prefix, this will by default install all', &
+ ' executables (tests and examples are excluded) which are part of the projects.', &
+ ' Libraries and module files are only installed for projects requiring the', &
+ ' installation of those components in the package manifest.', &
+ '', &
+ 'OPTIONS', &
+ ' --list list all installable targets for this project,', &
+ ' but do not install any of them', &
+ ' --profile PROF selects the compilation profile for the build.',&
+ ' Currently available profiles are "release" for',&
+ ' high optimization and "debug" for full debug options.',&
+ ' If --flag is not specified the "debug" flags are the',&
+ ' default. ',&
+ ' --flag FFLAGS selects compile arguments for the build. These are',&
+ ' added to the profile options if --profile is specified,',&
+ ' else these options override the defaults.',&
+ ' Note object and .mod directory locations are always',&
+ ' built in.',&
+ ' --no-rebuild do not rebuild project before installation', &
+ ' --prefix DIR path to installation directory (requires write access),', &
+ ' the default prefix on Unix systems is $HOME/.local', &
+ ' and %APPDATA%\local on Windows', &
+ ' --bindir DIR subdirectory to place executables in (default: bin)', &
+ ' --libdir DIR subdirectory to place libraries and archives in', &
+ ' (default: lib)', &
+ ' --includedir DIR subdirectory to place headers and module files in', &
+ ' (default: include)', &
+ ' --verbose print more information', &
+ '', &
+ 'EXAMPLES', &
+ ' 1. Install release version of project:', &
+ '', &
+ ' fpm install --profile release', &
+ '', &
+ ' 2. Install the project without rebuilding the executables:', &
+ '', &
+ ' fpm install --no-rebuild', &
+ '', &
+ ' 3. Install executables to a custom prefix into the exe directory:', &
+ '', &
+ ' fpm install --prefix $PWD --bindir exe', &
+ '' ]
+ end subroutine set_help
+
+ subroutine get_char_arg(var, arg)
+ character(len=:), allocatable, intent(out) :: var
+ character(len=*), intent(in) :: arg
+ var = sget(arg)
+ if (len_trim(var) == 0) deallocate(var)
+ end subroutine get_char_arg
+
+end module fpm_command_line
diff --git a/src/fpm_compiler.f90 b/src/fpm_compiler.f90
new file mode 100644
index 0000000..51cda20
--- /dev/null
+++ b/src/fpm_compiler.f90
@@ -0,0 +1,333 @@
+!># Define compiler command options
+!!
+!! This module defines compiler options to use for the debug and release builds.
+
+! vendor Fortran C Module output Module include OpenMP Free for OSS
+! compiler compiler directory directory
+! Gnu gfortran gcc -J -I -fopenmp X
+! Intel ifort icc -module -I -qopenmp X
+! Intel(Windows) ifort icc /module:path /I /Qopenmp X
+! Intel oneAPI ifx icx -module -I -qopenmp X
+! PGI pgfortran pgcc -module -I -mp X
+! NVIDIA nvfortran nvc -module -I -mp X
+! LLVM flang flang clang -module -I -mp X
+! LFortran lfortran --- ? ? ? X
+! Lahey/Futjitsu lfc ? -M -I -openmp ?
+! NAG nagfor ? -mdir -I -openmp x
+! Cray crayftn craycc -J -I -homp ?
+! IBM xlf90 ? -qmoddir -I -qsmp X
+! Oracle/Sun ? ? -moddir= -M -xopenmp ?
+! Silverfrost FTN95 ftn95 ? ? /MOD_PATH ? ?
+! Elbrus ? lcc -J -I -fopenmp ?
+! Hewlett Packard ? ? ? ? ? discontinued
+! Watcom ? ? ? ? ? discontinued
+! PathScale ? ? -module -I -mp discontinued
+! G95 ? ? -fmod= -I -fopenmp discontinued
+! Open64 ? ? -module -I -mp discontinued
+! Unisys ? ? ? ? ? discontinued
+module fpm_compiler
+use fpm_model, only: fpm_model_t
+use fpm_filesystem, only: join_path, basename
+implicit none
+public :: is_unknown_compiler
+public :: get_module_flags
+public :: get_default_compile_flags
+public :: get_debug_compile_flags
+public :: get_release_compile_flags
+
+enum, bind(C)
+ enumerator :: &
+ id_unknown, &
+ id_gcc, &
+ id_f95, &
+ id_caf, &
+ id_intel_classic, &
+ id_intel_llvm, &
+ id_pgi, &
+ id_nvhpc, &
+ id_nag, &
+ id_flang, &
+ id_ibmxl, &
+ id_cray, &
+ id_lahey, &
+ id_lfortran
+end enum
+integer, parameter :: compiler_enum = kind(id_unknown)
+
+contains
+
+subroutine get_default_compile_flags(compiler, release, flags)
+ character(len=*), intent(in) :: compiler
+ logical, intent(in) :: release
+ character(len=:), allocatable, intent(out) :: flags
+ integer :: id
+
+ id = get_compiler_id(compiler)
+ if (release) then
+ call get_release_compile_flags(id, flags)
+ else
+ call get_debug_compile_flags(id, flags)
+ end if
+
+end subroutine get_default_compile_flags
+
+subroutine get_release_compile_flags(id, flags)
+ integer(compiler_enum), intent(in) :: id
+ character(len=:), allocatable, intent(out) :: flags
+
+ select case(id)
+ case default
+ flags = ""
+
+ case(id_caf)
+ flags='&
+ & -O3&
+ & -Wimplicit-interface&
+ & -fPIC&
+ & -fmax-errors=1&
+ & -funroll-loops&
+ &'
+ case(id_gcc)
+ flags='&
+ & -O3&
+ & -Wimplicit-interface&
+ & -fPIC&
+ & -fmax-errors=1&
+ & -funroll-loops&
+ & -fcoarray=single&
+ &'
+ case(id_f95)
+ flags='&
+ & -O3&
+ & -Wimplicit-interface&
+ & -fPIC&
+ & -fmax-errors=1&
+ & -ffast-math&
+ & -funroll-loops&
+ &'
+ case(id_nvhpc)
+ flags = '&
+ & -Mbackslash&
+ &'
+ case(id_intel_classic)
+ flags = '&
+ & -fp-model precise&
+ & -pc 64&
+ & -align all&
+ & -error-limit 1&
+ & -reentrancy threaded&
+ & -nogen-interfaces&
+ & -assume byterecl&
+ &'
+ case(id_nag)
+ flags = ' &
+ & -O4&
+ & -coarray=single&
+ & -PIC&
+ &'
+ end select
+end subroutine get_release_compile_flags
+
+subroutine get_debug_compile_flags(id, flags)
+ integer(compiler_enum), intent(in) :: id
+ character(len=:), allocatable, intent(out) :: flags
+
+ select case(id)
+ case default
+ flags = ""
+
+ case(id_caf)
+ flags = '&
+ & -Wall&
+ & -Wextra&
+ & -Wimplicit-interface&
+ & -fPIC -fmax-errors=1&
+ & -g&
+ & -fcheck=bounds&
+ & -fcheck=array-temps&
+ & -fbacktrace&
+ &'
+
+ case(id_gcc)
+ flags = '&
+ & -Wall&
+ & -Wextra&
+ & -Wimplicit-interface&
+ & -fPIC -fmax-errors=1&
+ & -g&
+ & -fcheck=bounds&
+ & -fcheck=array-temps&
+ & -fbacktrace&
+ & -fcoarray=single&
+ &'
+
+ case(id_f95)
+ flags = '&
+ & -Wall&
+ & -Wextra&
+ & -Wimplicit-interface&
+ & -fPIC -fmax-errors=1&
+ & -g&
+ & -fcheck=bounds&
+ & -fcheck=array-temps&
+ & -Wno-maybe-uninitialized -Wno-uninitialized&
+ & -fbacktrace&
+ &'
+
+ case(id_nvhpc)
+ flags = '&
+ & -Minform=inform&
+ & -Mbackslash&
+ & -g&
+ & -Mbounds&
+ & -Mchkptr&
+ & -Mchkstk&
+ & -traceback&
+ &'
+
+ case(id_intel_classic)
+ flags = '&
+ & -warn all&
+ & -check:all:noarg_temp_created&
+ & -error-limit 1&
+ & -O0&
+ & -g&
+ & -assume byterecl&
+ & -traceback&
+ &'
+
+ case(id_nag)
+ flags = '&
+ & -g&
+ & -C=all&
+ & -O0&
+ & -gline&
+ & -coarray=single&
+ & -PIC&
+ &'
+ end select
+end subroutine get_debug_compile_flags
+
+subroutine get_module_flags(compiler, modpath, flags)
+ character(len=*), intent(in) :: compiler
+ character(len=*), intent(in) :: modpath
+ character(len=:), allocatable, intent(out) :: flags
+ integer(compiler_enum) :: id
+
+ id = get_compiler_id(compiler)
+
+ select case(id)
+ case default
+ flags=' -module '//modpath//' -I '//modpath
+
+ case(id_caf, id_gcc, id_f95, id_cray)
+ flags=' -J '//modpath//' -I '//modpath
+
+ case(id_intel_classic, id_intel_llvm, id_nvhpc, id_pgi, id_flang)
+ flags=' -module '//modpath//' -I '//modpath
+
+ case(id_lahey)
+ flags=' -M '//modpath//' -I '//modpath
+
+ case(id_nag)
+ flags=' -mdir '//modpath//' -I '//modpath !
+
+ case(id_ibmxl)
+ flags=' -qmoddir '//modpath//' -I '//modpath
+
+ end select
+
+end subroutine get_module_flags
+
+function get_compiler_id(compiler) result(id)
+ character(len=*), intent(in) :: compiler
+ integer(kind=compiler_enum) :: id
+
+ if (check_compiler(compiler, "gfortran")) then
+ id = id_gcc
+ return
+ end if
+
+ if (check_compiler(compiler, "f95")) then
+ id = id_f95
+ return
+ end if
+
+ if (check_compiler(compiler, "caf")) then
+ id = id_caf
+ return
+ end if
+
+ if (check_compiler(compiler, "ifort")) then
+ id = id_intel_classic
+ return
+ end if
+
+ if (check_compiler(compiler, "ifx")) then
+ id = id_intel_llvm
+ return
+ end if
+
+ if (check_compiler(compiler, "nvfortran")) then
+ id = id_nvhpc
+ return
+ end if
+
+ if (check_compiler(compiler, "pgfortran") &
+ & .or. check_compiler(compiler, "pgf90") &
+ & .or. check_compiler(compiler, "pgf95")) then
+ id = id_pgi
+ return
+ end if
+
+ if (check_compiler(compiler, "nagfor")) then
+ id = id_nag
+ return
+ end if
+
+ if (check_compiler(compiler, "flang")) then
+ id = id_flang
+ return
+ end if
+
+ if (check_compiler(compiler, "xlf90")) then
+ id = id_ibmxl
+ return
+ end if
+
+ if (check_compiler(compiler, "crayftn")) then
+ id = id_cray
+ return
+ end if
+
+ if (check_compiler(compiler, "lfc")) then
+ id = id_lahey
+ return
+ end if
+
+ if (check_compiler(compiler, "lfort")) then
+ id = id_lfortran
+ return
+ end if
+
+ id = id_unknown
+
+end function get_compiler_id
+
+function check_compiler(compiler, expected) result(match)
+ character(len=*), intent(in) :: compiler
+ character(len=*), intent(in) :: expected
+ logical :: match
+ match = compiler == expected
+ if (.not. match) then
+ match = index(basename(compiler), expected) > 0
+ end if
+end function check_compiler
+
+function is_unknown_compiler(compiler) result(is_unknown)
+ character(len=*), intent(in) :: compiler
+ logical :: is_unknown
+ is_unknown = get_compiler_id(compiler) == id_unknown
+end function is_unknown_compiler
+
+end module fpm_compiler
diff --git a/src/fpm_environment.f90 b/src/fpm_environment.f90
new file mode 100644
index 0000000..0408ec4
--- /dev/null
+++ b/src/fpm_environment.f90
@@ -0,0 +1,185 @@
+!> This module contains procedures that interact with the programming environment.
+!!
+!! * [get_os_type] -- Determine the OS type
+!! * [get_env] -- return the value of an environment variable
+module fpm_environment
+ implicit none
+ private
+ public :: get_os_type
+ public :: os_is_unix
+ public :: run
+ public :: get_env
+
+ integer, parameter, public :: OS_UNKNOWN = 0
+ integer, parameter, public :: OS_LINUX = 1
+ integer, parameter, public :: OS_MACOS = 2
+ integer, parameter, public :: OS_WINDOWS = 3
+ integer, parameter, public :: OS_CYGWIN = 4
+ integer, parameter, public :: OS_SOLARIS = 5
+ integer, parameter, public :: OS_FREEBSD = 6
+contains
+ !> Determine the OS type
+ integer function get_os_type() result(r)
+ !!
+ !! Returns one of OS_UNKNOWN, OS_LINUX, OS_MACOS, OS_WINDOWS, OS_CYGWIN,
+ !! OS_SOLARIS, OS_FREEBSD.
+ !!
+ !! At first, the environment variable `OS` is checked, which is usually
+ !! found on Windows. Then, `OSTYPE` is read in and compared with common
+ !! names. If this fails too, check the existence of files that can be
+ !! found on specific system types only.
+ !!
+ !! Returns OS_UNKNOWN if the operating system cannot be determined.
+ character(len=32) :: val
+ integer :: length, rc
+ logical :: file_exists
+
+ r = OS_UNKNOWN
+
+ ! Check environment variable `OS`.
+ call get_environment_variable('OS', val, length, rc)
+
+ if (rc == 0 .and. length > 0 .and. index(val, 'Windows_NT') > 0) then
+ r = OS_WINDOWS
+ return
+ end if
+
+ ! Check environment variable `OSTYPE`.
+ call get_environment_variable('OSTYPE', val, length, rc)
+
+ if (rc == 0 .and. length > 0) then
+ ! Linux
+ if (index(val, 'linux') > 0) then
+ r = OS_LINUX
+ return
+ end if
+
+ ! macOS
+ if (index(val, 'darwin') > 0) then
+ r = OS_MACOS
+ return
+ end if
+
+ ! Windows, MSYS, MinGW, Git Bash
+ if (index(val, 'win') > 0 .or. index(val, 'msys') > 0) then
+ r = OS_WINDOWS
+ return
+ end if
+
+ ! Cygwin
+ if (index(val, 'cygwin') > 0) then
+ r = OS_CYGWIN
+ return
+ end if
+
+ ! Solaris, OpenIndiana, ...
+ if (index(val, 'SunOS') > 0 .or. index(val, 'solaris') > 0) then
+ r = OS_SOLARIS
+ return
+ end if
+
+ ! FreeBSD
+ if (index(val, 'FreeBSD') > 0 .or. index(val, 'freebsd') > 0) then
+ r = OS_FREEBSD
+ return
+ end if
+ end if
+
+ ! Linux
+ inquire (file='/etc/os-release', exist=file_exists)
+
+ if (file_exists) then
+ r = OS_LINUX
+ return
+ end if
+
+ ! macOS
+ inquire (file='/usr/bin/sw_vers', exist=file_exists)
+
+ if (file_exists) then
+ r = OS_MACOS
+ return
+ end if
+
+ ! FreeBSD
+ inquire (file='/bin/freebsd-version', exist=file_exists)
+
+ if (file_exists) then
+ r = OS_FREEBSD
+ return
+ end if
+ end function get_os_type
+
+ !> Compare the output of [[get_os_type]] or the optional
+ !! passed INTEGER value to the value for OS_WINDOWS
+ !! and return .TRUE. if they match and .FALSE. otherwise
+ logical function os_is_unix(os) result(unix)
+ integer, intent(in), optional :: os
+ integer :: build_os
+ if (present(os)) then
+ build_os = os
+ else
+ build_os = get_os_type()
+ end if
+ unix = os /= OS_WINDOWS
+ end function os_is_unix
+
+ !> echo command string and pass it to the system for execution
+ subroutine run(cmd,echo)
+ character(len=*), intent(in) :: cmd
+ logical,intent(in),optional :: echo
+ logical :: echo_local
+ integer :: stat
+
+ if(present(echo))then
+ echo_local=echo
+ else
+ echo_local=.true.
+ endif
+ if(echo_local) print *, '+ ', cmd
+
+ call execute_command_line(cmd, exitstat=stat)
+ if (stat /= 0) then
+ print *, 'Command failed'
+ error stop
+ end if
+ end subroutine run
+
+ !> get named environment variable value. It it is blank or
+ !! not set return the optional default value
+ function get_env(NAME,DEFAULT) result(VALUE)
+ implicit none
+ !> name of environment variable to get the value of
+ character(len=*),intent(in) :: NAME
+ !> default value to return if the requested value is undefined or blank
+ character(len=*),intent(in),optional :: DEFAULT
+ !> the returned value
+ character(len=:),allocatable :: VALUE
+ integer :: howbig
+ integer :: stat
+ integer :: length
+ ! get length required to hold value
+ length=0
+ if(NAME.ne.'')then
+ call get_environment_variable(NAME, length=howbig,status=stat,trim_name=.true.)
+ select case (stat)
+ case (1)
+ !*!print *, NAME, " is not defined in the environment. Strange..."
+ VALUE=''
+ case (2)
+ !*!print *, "This processor doesn't support environment variables. Boooh!"
+ VALUE=''
+ case default
+ ! make string to hold value of sufficient size
+ allocate(character(len=max(howbig,1)) :: VALUE)
+ ! get value
+ call get_environment_variable(NAME,VALUE,status=stat,trim_name=.true.)
+ if(stat.ne.0)VALUE=''
+ end select
+ else
+ VALUE=''
+ endif
+ if(VALUE.eq.''.and.present(DEFAULT))VALUE=DEFAULT
+ end function get_env
+
+end module fpm_environment
diff --git a/src/fpm_filesystem.f90 b/src/fpm_filesystem.f90
new file mode 100644
index 0000000..6acd383
--- /dev/null
+++ b/src/fpm_filesystem.f90
@@ -0,0 +1,612 @@
+!> This module contains general routines for interacting with the file system
+!!
+module fpm_filesystem
+use,intrinsic :: iso_fortran_env, only : stdin=>input_unit, stdout=>output_unit, stderr=>error_unit
+ use fpm_environment, only: get_os_type, &
+ OS_UNKNOWN, OS_LINUX, OS_MACOS, OS_WINDOWS, &
+ OS_CYGWIN, OS_SOLARIS, OS_FREEBSD
+ use fpm_strings, only: f_string, replace, string_t, split
+ implicit none
+ private
+ public :: basename, canon_path, dirname, is_dir, join_path, number_of_rows, read_lines, list_files, env_variable, &
+ mkdir, exists, get_temp_filename, windows_path, unix_path, getline, delete_file, to_fortran_name
+ public :: fileopen, fileclose, filewrite, warnwrite
+
+ integer, parameter :: LINE_BUFFER_LEN = 1000
+
+contains
+
+
+!> return value of environment variable
+subroutine env_variable(var, name)
+ character(len=:), allocatable, intent(out) :: var
+ character(len=*), intent(in) :: name
+ integer :: length, stat
+
+ call get_environment_variable(name, length=length, status=stat)
+ if (stat /= 0) return
+
+ allocate(character(len=length) :: var)
+
+ if (length > 0) then
+ call get_environment_variable(name, var, status=stat)
+ if (stat /= 0) then
+ deallocate(var)
+ return
+ end if
+ end if
+
+end subroutine env_variable
+
+
+!> Extract filename from path with/without suffix
+function basename(path,suffix) result (base)
+
+ character(*), intent(In) :: path
+ logical, intent(in), optional :: suffix
+ character(:), allocatable :: base
+
+ character(:), allocatable :: file_parts(:)
+ logical :: with_suffix
+
+ if (.not.present(suffix)) then
+ with_suffix = .true.
+ else
+ with_suffix = suffix
+ end if
+
+ if (with_suffix) then
+ call split(path,file_parts,delimiters='\/')
+ if(size(file_parts).gt.0)then
+ base = trim(file_parts(size(file_parts)))
+ else
+ base = ''
+ endif
+ else
+ call split(path,file_parts,delimiters='\/.')
+ if(size(file_parts).ge.2)then
+ base = trim(file_parts(size(file_parts)-1))
+ else
+ base = ''
+ endif
+ end if
+
+end function basename
+
+
+!> Canonicalize path for comparison
+!! * Handles path string redundancies
+!! * Does not test existence of path
+!!
+!! To be replaced by realpath/_fullname in stdlib_os
+!!
+!! FIXME: Lot's of ugly hacks following here
+function canon_path(path)
+ character(len=*), intent(in) :: path
+ character(len=:), allocatable :: canon_path
+ character(len=:), allocatable :: nixpath
+
+ integer :: ii, istart, iend, stat, nn, last
+ logical :: is_path, absolute
+
+ nixpath = unix_path(path)
+
+ istart = 0
+ nn = 0
+ iend = 0
+ absolute = nixpath(1:1) == "/"
+ if (absolute) then
+ canon_path = "/"
+ else
+ canon_path = ""
+ end if
+
+ do while(iend < len(nixpath))
+ call next(nixpath, istart, iend, is_path)
+ if (is_path) then
+ select case(nixpath(istart:iend))
+ case(".", "") ! always drop empty paths
+ case("..")
+ if (nn > 0) then
+ last = scan(canon_path(:len(canon_path)-1), "/", back=.true.)
+ canon_path = canon_path(:last)
+ nn = nn - 1
+ else
+ if (.not. absolute) then
+ canon_path = canon_path // nixpath(istart:iend) // "/"
+ end if
+ end if
+ case default
+ nn = nn + 1
+ canon_path = canon_path // nixpath(istart:iend) // "/"
+ end select
+ end if
+ end do
+
+ if (len(canon_path) == 0) canon_path = "."
+ if (len(canon_path) > 1 .and. canon_path(len(canon_path):) == "/") then
+ canon_path = canon_path(:len(canon_path)-1)
+ end if
+
+contains
+
+ subroutine next(string, istart, iend, is_path)
+ character(len=*), intent(in) :: string
+ integer, intent(inout) :: istart
+ integer, intent(inout) :: iend
+ logical, intent(inout) :: is_path
+
+ integer :: ii, nn
+ character :: tok, last
+
+ nn = len(string)
+
+ if (iend >= nn) then
+ istart = nn
+ iend = nn
+ return
+ end if
+
+ ii = min(iend + 1, nn)
+ tok = string(ii:ii)
+
+ is_path = tok /= '/'
+
+ if (.not.is_path) then
+ is_path = .false.
+ istart = ii
+ iend = ii
+ return
+ end if
+
+ istart = ii
+ do ii = min(iend + 1, nn), nn
+ tok = string(ii:ii)
+ select case(tok)
+ case('/')
+ exit
+ case default
+ iend = ii
+ cycle
+ end select
+ end do
+
+ end subroutine next
+end function canon_path
+
+
+!> Extract dirname from path
+function dirname(path) result (dir)
+ character(*), intent(in) :: path
+ character(:), allocatable :: dir
+
+ dir = path(1:scan(path,'/\',back=.true.))
+
+end function dirname
+
+
+!> test if a name matches an existing directory path
+logical function is_dir(dir)
+ character(*), intent(in) :: dir
+ integer :: stat
+
+ select case (get_os_type())
+
+ case (OS_UNKNOWN, OS_LINUX, OS_MACOS, OS_CYGWIN, OS_SOLARIS, OS_FREEBSD)
+ call execute_command_line("test -d " // dir , exitstat=stat)
+
+ case (OS_WINDOWS)
+ call execute_command_line('cmd /c "if not exist ' // windows_path(dir) // '\ exit /B 1"', exitstat=stat)
+
+ end select
+
+ is_dir = (stat == 0)
+
+end function is_dir
+
+
+!> Construct path by joining strings with os file separator
+function join_path(a1,a2,a3,a4,a5) result(path)
+
+ character(len=*), intent(in) :: a1, a2
+ character(len=*), intent(in), optional :: a3, a4, a5
+ character(len=:), allocatable :: path
+ character(len=1) :: filesep
+
+ select case (get_os_type())
+ case (OS_UNKNOWN, OS_LINUX, OS_MACOS, OS_CYGWIN, OS_SOLARIS, OS_FREEBSD)
+ filesep = '/'
+ case (OS_WINDOWS)
+ filesep = '\'
+ end select
+
+ path = a1 // filesep // a2
+
+ if (present(a3)) then
+ path = path // filesep // a3
+ else
+ return
+ end if
+
+ if (present(a4)) then
+ path = path // filesep // a4
+ else
+ return
+ end if
+
+ if (present(a5)) then
+ path = path // filesep // a5
+ else
+ return
+ end if
+
+end function join_path
+
+
+!> Determine number or rows in a file given a LUN
+integer function number_of_rows(s) result(nrows)
+ integer,intent(in)::s
+ integer :: ios
+ character(len=100) :: r
+ rewind(s)
+ nrows = 0
+ do
+ read(s, '(A)', iostat=ios) r
+ if (ios /= 0) exit
+ nrows = nrows + 1
+ end do
+ rewind(s)
+end function number_of_rows
+
+
+!> read lines into an array of TYPE(STRING_T) variables
+function read_lines(fh) result(lines)
+ integer, intent(in) :: fh
+ type(string_t), allocatable :: lines(:)
+
+ integer :: i
+ character(LINE_BUFFER_LEN) :: line_buffer
+
+ allocate(lines(number_of_rows(fh)))
+ do i = 1, size(lines)
+ read(fh, '(A)') line_buffer
+ lines(i)%s = trim(line_buffer)
+ end do
+
+end function read_lines
+
+!> Create a directory. Create subdirectories as needed
+subroutine mkdir(dir)
+ character(len=*), intent(in) :: dir
+ integer :: stat
+
+ if (is_dir(dir)) return
+
+ select case (get_os_type())
+ case (OS_UNKNOWN, OS_LINUX, OS_MACOS, OS_CYGWIN, OS_SOLARIS, OS_FREEBSD)
+ call execute_command_line('mkdir -p ' // dir, exitstat=stat)
+ write (*, '(" + ",2a)') 'mkdir -p ' // dir
+
+ case (OS_WINDOWS)
+ call execute_command_line("mkdir " // windows_path(dir), exitstat=stat)
+ write (*, '(" + ",2a)') 'mkdir ' // windows_path(dir)
+ end select
+
+ if (stat /= 0) then
+ print *, 'execute_command_line() failed'
+ error stop
+ end if
+end subroutine mkdir
+
+
+!> Get file & directory names in directory `dir`.
+!!
+!! - File/directory names return are relative to cwd, ie. preprended with `dir`
+!! - Includes files starting with `.` except current directory and parent directory
+!!
+recursive subroutine list_files(dir, files, recurse)
+ character(len=*), intent(in) :: dir
+ type(string_t), allocatable, intent(out) :: files(:)
+ logical, intent(in), optional :: recurse
+
+ integer :: stat, fh, i
+ character(:), allocatable :: temp_file
+ type(string_t), allocatable :: dir_files(:)
+ type(string_t), allocatable :: sub_dir_files(:)
+
+ if (.not. is_dir(dir)) then
+ allocate (files(0))
+ return
+ end if
+
+ allocate (temp_file, source=get_temp_filename())
+
+ select case (get_os_type())
+ case (OS_UNKNOWN, OS_LINUX, OS_MACOS, OS_CYGWIN, OS_SOLARIS, OS_FREEBSD)
+ call execute_command_line('ls -A ' // dir // ' > ' // temp_file, &
+ exitstat=stat)
+ case (OS_WINDOWS)
+ call execute_command_line('dir /b ' // windows_path(dir) // ' > ' // temp_file, &
+ exitstat=stat)
+ end select
+
+ if (stat /= 0) then
+ print *, 'execute_command_line() failed'
+ error stop
+ end if
+
+ open (newunit=fh, file=temp_file, status='old')
+ files = read_lines(fh)
+ close(fh,status="delete")
+
+ do i=1,size(files)
+ files(i)%s = join_path(dir,files(i)%s)
+ end do
+
+ if (present(recurse)) then
+ if (recurse) then
+
+ allocate(sub_dir_files(0))
+
+ do i=1,size(files)
+ if (is_dir(files(i)%s)) then
+
+ call list_files(files(i)%s, dir_files, recurse=.true.)
+ sub_dir_files = [sub_dir_files, dir_files]
+
+ end if
+ end do
+
+ files = [files, sub_dir_files]
+
+ end if
+ end if
+
+end subroutine list_files
+
+
+!> test if pathname already exists
+logical function exists(filename) result(r)
+ character(len=*), intent(in) :: filename
+ inquire(file=filename, exist=r)
+end function
+
+
+!> Get a unused temporary filename
+!! Calls posix 'tempnam' - not recommended, but
+!! we have no security concerns for this application
+!! and use here is temporary.
+!! Works with MinGW
+function get_temp_filename() result(tempfile)
+ !
+ use iso_c_binding, only: c_ptr, C_NULL_PTR, c_f_pointer
+ character(:), allocatable :: tempfile
+
+ type(c_ptr) :: c_tempfile_ptr
+ character(len=1), pointer :: c_tempfile(:)
+
+ interface
+
+ function c_tempnam(dir,pfx) result(tmp) bind(c,name="tempnam")
+ import
+ type(c_ptr), intent(in), value :: dir
+ type(c_ptr), intent(in), value :: pfx
+ type(c_ptr) :: tmp
+ end function c_tempnam
+
+ subroutine c_free(ptr) BIND(C,name="free")
+ import
+ type(c_ptr), value :: ptr
+ end subroutine c_free
+
+ end interface
+
+ c_tempfile_ptr = c_tempnam(C_NULL_PTR, C_NULL_PTR)
+ call c_f_pointer(c_tempfile_ptr,c_tempfile,[LINE_BUFFER_LEN])
+
+ tempfile = f_string(c_tempfile)
+
+ call c_free(c_tempfile_ptr)
+
+end function get_temp_filename
+
+
+!> Replace file system separators for windows
+function windows_path(path) result(winpath)
+
+ character(*), intent(in) :: path
+ character(:), allocatable :: winpath
+
+ integer :: idx
+
+ winpath = path
+
+ idx = index(winpath,'/')
+ do while(idx > 0)
+ winpath(idx:idx) = '\'
+ idx = index(winpath,'/')
+ end do
+
+end function windows_path
+
+
+!> Replace file system separators for unix
+function unix_path(path) result(nixpath)
+
+ character(*), intent(in) :: path
+ character(:), allocatable :: nixpath
+
+ integer :: idx
+
+ nixpath = path
+
+ idx = index(nixpath,'\')
+ do while(idx > 0)
+ nixpath(idx:idx) = '/'
+ idx = index(nixpath,'\')
+ end do
+
+end function unix_path
+
+
+!> read a line of arbitrary length into a CHARACTER variable from the specified LUN
+subroutine getline(unit, line, iostat, iomsg)
+
+ !> Formatted IO unit
+ integer, intent(in) :: unit
+
+ !> Line to read
+ character(len=:), allocatable, intent(out) :: line
+
+ !> Status of operation
+ integer, intent(out) :: iostat
+
+ !> Error message
+ character(len=:), allocatable, optional :: iomsg
+
+ character(len=LINE_BUFFER_LEN) :: buffer
+ character(len=LINE_BUFFER_LEN) :: msg
+ integer :: size
+ integer :: stat
+
+ allocate(character(len=0) :: line)
+ do
+ read(unit, '(a)', advance='no', iostat=stat, iomsg=msg, size=size) &
+ & buffer
+ if (stat > 0) exit
+ line = line // buffer(:size)
+ if (stat < 0) then
+ if (is_iostat_eor(stat)) then
+ stat = 0
+ end if
+ exit
+ end if
+ end do
+
+ if (stat /= 0) then
+ if (present(iomsg)) iomsg = trim(msg)
+ end if
+ iostat = stat
+
+end subroutine getline
+
+
+!> delete a file by filename
+subroutine delete_file(file)
+ character(len=*), intent(in) :: file
+ logical :: exist
+ integer :: unit
+ inquire(file=file, exist=exist)
+ if (exist) then
+ open(file=file, newunit=unit)
+ close(unit, status="delete")
+ end if
+end subroutine delete_file
+
+!> write trimmed character data to a file if it does not exist
+subroutine warnwrite(fname,data)
+character(len=*),intent(in) :: fname
+character(len=*),intent(in) :: data(:)
+
+ if(.not.exists(fname))then
+ call filewrite(fname,data)
+ else
+ write(stderr,'(*(g0,1x))')'<INFO> ',fname,&
+ & 'already exists. Not overwriting'
+ endif
+
+end subroutine warnwrite
+
+!> procedure to open filename as a sequential "text" file
+subroutine fileopen(filename,lun,ier)
+
+character(len=*),intent(in) :: filename
+integer,intent(out) :: lun
+integer,intent(out),optional :: ier
+integer :: ios
+character(len=256) :: message
+
+ message=' '
+ ios=0
+ if(filename.ne.' ')then
+ open(file=filename, &
+ & newunit=lun, &
+ & form='formatted', & ! FORM = FORMATTED | UNFORMATTED
+ & access='sequential', & ! ACCESS = SEQUENTIAL| DIRECT | STREAM
+ & action='write', & ! ACTION = READ|WRITE| READWRITE
+ & position='rewind', & ! POSITION= ASIS | REWIND | APPEND
+ & status='new', & ! STATUS = NEW| REPLACE| OLD| SCRATCH| UNKNOWN
+ & iostat=ios, &
+ & iomsg=message)
+ else
+ lun=stdout
+ ios=0
+ endif
+ if(ios.ne.0)then
+ write(stderr,'(*(a:,1x))')&
+ & '<ERROR> *filewrite*:',filename,trim(message)
+ lun=-1
+ if(present(ier))then
+ ier=ios
+ else
+ stop 1
+ endif
+ endif
+
+end subroutine fileopen
+
+!> simple close of a LUN. On error show message and stop (by default)
+subroutine fileclose(lun,ier)
+integer,intent(in) :: lun
+integer,intent(out),optional :: ier
+character(len=256) :: message
+integer :: ios
+ if(lun.ne.-1)then
+ close(unit=lun,iostat=ios,iomsg=message)
+ if(ios.ne.0)then
+ write(stderr,'(*(a:,1x))')'<ERROR> *filewrite*:',trim(message)
+ if(present(ier))then
+ ier=ios
+ else
+ stop 2
+ endif
+ endif
+ endif
+end subroutine fileclose
+
+!> procedure to write filedata to file filename
+subroutine filewrite(filename,filedata)
+
+character(len=*),intent(in) :: filename
+character(len=*),intent(in) :: filedata(:)
+integer :: lun, i, ios
+character(len=256) :: message
+ call fileopen(filename,lun)
+ if(lun.ne.-1)then ! program currently stops on error on open, but might
+ ! want it to continue so -1 (unallowed LUN) indicates error
+ ! write file
+ do i=1,size(filedata)
+ write(lun,'(a)',iostat=ios,iomsg=message)trim(filedata(i))
+ if(ios.ne.0)then
+ write(stderr,'(*(a:,1x))')&
+ & '<ERROR> *filewrite*:',filename,trim(message)
+ stop 4
+ endif
+ enddo
+ endif
+ ! close file
+ call fileclose(lun)
+
+end subroutine filewrite
+
+!> Returns string with special characters replaced with an underscore.
+!! For now, only a hyphen is treated as a special character, but this can be
+!! expanded to other characters if needed.
+pure function to_fortran_name(string) result(res)
+ character(*), intent(in) :: string
+ character(len(string)) :: res
+ character, parameter :: SPECIAL_CHARACTERS(*) = ['-']
+ res = replace(string, SPECIAL_CHARACTERS, '_')
+end function to_fortran_name
+
+end module fpm_filesystem
diff --git a/src/fpm_model.f90 b/src/fpm_model.f90
new file mode 100644
index 0000000..bfb0115
--- /dev/null
+++ b/src/fpm_model.f90
@@ -0,0 +1,293 @@
+!># The fpm package model
+!>
+!> Defines the fpm model data types which encapsulate all information
+!> required to correctly build a package and its dependencies.
+!>
+!> The process (see `[[build_model(subroutine)]]`) for generating a valid `[[fpm_model]]` involves
+!> source files discovery ([[fpm_sources]]) and parsing ([[fpm_source_parsing]]).
+!>
+!> Once a valid `[[fpm_model]]` has been constructed, it may be passed to `[[fpm_targets:targets_from_sources]]` to
+!> generate a list of build targets for the backend.
+!>
+!>### Enumerations
+!>
+!> __Source type:__ `FPM_UNIT_*`
+!> Describes the type of source file — determines build target generation
+!>
+!> __Source scope:__ `FPM_SCOPE_*`
+!> Describes the scoping rules for using modules — controls module dependency resolution
+!>
+module fpm_model
+use iso_fortran_env, only: int64
+use fpm_strings, only: string_t, str
+use fpm_dependency, only: dependency_tree_t
+implicit none
+
+private
+public :: fpm_model_t, srcfile_t, show_model
+
+public :: FPM_UNIT_UNKNOWN, FPM_UNIT_PROGRAM, FPM_UNIT_MODULE, &
+ FPM_UNIT_SUBMODULE, FPM_UNIT_SUBPROGRAM, FPM_UNIT_CSOURCE, &
+ FPM_UNIT_CHEADER, FPM_SCOPE_UNKNOWN, FPM_SCOPE_LIB, &
+ FPM_SCOPE_DEP, FPM_SCOPE_APP, FPM_SCOPE_EXAMPLE, FPM_SCOPE_TEST
+
+!> Source type unknown
+integer, parameter :: FPM_UNIT_UNKNOWN = -1
+!> Source type is fortran program
+integer, parameter :: FPM_UNIT_PROGRAM = 1
+!> Source type is fortran module
+integer, parameter :: FPM_UNIT_MODULE = 2
+!> Source type is fortran submodule
+integer, parameter :: FPM_UNIT_SUBMODULE = 3
+!> Source type is fortran subprogram
+integer, parameter :: FPM_UNIT_SUBPROGRAM = 4
+!> Source type is c source file
+integer, parameter :: FPM_UNIT_CSOURCE = 5
+!> Source type is c header file
+integer, parameter :: FPM_UNIT_CHEADER = 6
+
+
+!> Source has no module-use scope
+integer, parameter :: FPM_SCOPE_UNKNOWN = -1
+!> Module-use scope is library/dependency modules only
+integer, parameter :: FPM_SCOPE_LIB = 1
+!> Module-use scope is library/dependency modules only
+integer, parameter :: FPM_SCOPE_DEP = 2
+!> Module-use scope is library/dependency and app modules
+integer, parameter :: FPM_SCOPE_APP = 3
+!> Module-use scope is library/dependency and test modules
+integer, parameter :: FPM_SCOPE_TEST = 4
+integer, parameter :: FPM_SCOPE_EXAMPLE = 5
+
+
+!> Type for describing a source file
+type srcfile_t
+ !> File path relative to cwd
+ character(:), allocatable :: file_name
+
+ !> Name of executable for FPM_UNIT_PROGRAM
+ character(:), allocatable :: exe_name
+
+ !> Target module-use scope
+ integer :: unit_scope = FPM_SCOPE_UNKNOWN
+
+ !> Modules provided by this source file (lowerstring)
+ type(string_t), allocatable :: modules_provided(:)
+
+ !> Type of source unit
+ integer :: unit_type = FPM_UNIT_UNKNOWN
+
+ !> Modules USEd by this source file (lowerstring)
+ type(string_t), allocatable :: modules_used(:)
+
+ !> Files INCLUDEd by this source file
+ type(string_t), allocatable :: include_dependencies(:)
+
+ !> Native libraries to link against
+ type(string_t), allocatable :: link_libraries(:)
+
+ !> Current hash
+ integer(int64) :: digest
+
+end type srcfile_t
+
+
+!> Type for describing a single package
+type package_t
+
+ !> Name of package
+ character(:), allocatable :: name
+
+ !> Array of sources
+ type(srcfile_t), allocatable :: sources(:)
+
+end type package_t
+
+
+!> Type describing everything required to build
+!> the root package and its dependencies.
+type :: fpm_model_t
+
+ !> Name of root package
+ character(:), allocatable :: package_name
+
+ !> Array of packages (including the root package)
+ type(package_t), allocatable :: packages(:)
+
+ !> Command line name to invoke fortran compiler
+ character(:), allocatable :: fortran_compiler
+
+ !> Command line flags passed to fortran for compilation
+ character(:), allocatable :: fortran_compile_flags
+
+ !> Base directory for build
+ character(:), allocatable :: output_directory
+
+ !> Include directories
+ type(string_t), allocatable :: include_dirs(:)
+
+ !> Native libraries to link against
+ type(string_t), allocatable :: link_libraries(:)
+
+ !> Project dependencies
+ type(dependency_tree_t) :: deps
+
+end type fpm_model_t
+
+contains
+
+
+function info_package(p) result(s)
+ ! Returns representation of package_t
+ type(package_t), intent(in) :: p
+ character(:), allocatable :: s
+
+ integer :: i
+
+ s = s // 'package_t('
+ s = s // 'name="' // p%name //'"'
+ s = s // ', sources=['
+ do i = 1, size(p%sources)
+ s = s // info_srcfile(p%sources(i))
+ if (i < size(p%sources)) s = s // ", "
+ end do
+ s = s // "]"
+ s = s // ")"
+
+end function info_package
+
+function info_srcfile(source) result(s)
+ type(srcfile_t), intent(in) :: source
+ character(:), allocatable :: s
+ integer :: i
+ !type srcfile_t
+ s = "srcfile_t("
+ ! character(:), allocatable :: file_name
+ s = s // 'file_name="' // source%file_name // '"'
+ ! character(:), allocatable :: exe_name
+ s = s // ', exe_name="' // source%exe_name // '"'
+ ! integer :: unit_scope = FPM_SCOPE_UNKNOWN
+ s = s // ", unit_scope="
+ select case(source%unit_scope)
+ case (FPM_SCOPE_UNKNOWN)
+ s = s // "FPM_SCOPE_UNKNOWN"
+ case (FPM_SCOPE_LIB)
+ s = s // "FPM_SCOPE_LIB"
+ case (FPM_SCOPE_DEP)
+ s = s // "FPM_SCOPE_DEP"
+ case (FPM_SCOPE_APP)
+ s = s // "FPM_SCOPE_APP"
+ case (FPM_SCOPE_TEST)
+ s = s // "FPM_SCOPE_TEST"
+ case (FPM_SCOPE_EXAMPLE)
+ s = s // "FPM_SCOPE_EXAMPLE"
+ case default
+ s = s // "INVALID"
+ end select
+ ! type(string_t), allocatable :: modules_provided(:)
+ s = s // ", modules_provided=["
+ do i = 1, size(source%modules_provided)
+ s = s // '"' // source%modules_provided(i)%s // '"'
+ if (i < size(source%modules_provided)) s = s // ", "
+ end do
+ s = s // "]"
+ ! integer :: unit_type = FPM_UNIT_UNKNOWN
+ s = s // ", unit_type="
+ select case(source%unit_type)
+ case (FPM_UNIT_UNKNOWN)
+ s = s // "FPM_UNIT_UNKNOWN"
+ case (FPM_UNIT_PROGRAM)
+ s = s // "FPM_UNIT_PROGRAM"
+ case (FPM_UNIT_MODULE)
+ s = s // "FPM_UNIT_MODULE"
+ case (FPM_UNIT_SUBMODULE)
+ s = s // "FPM_UNIT_SUBMODULE"
+ case (FPM_UNIT_SUBPROGRAM)
+ s = s // "FPM_UNIT_SUBPROGRAM"
+ case (FPM_UNIT_CSOURCE)
+ s = s // "FPM_UNIT_CSOURCE"
+ case (FPM_UNIT_CHEADER)
+ s = s // "FPM_UNIT_CHEADER"
+ case default
+ s = s // "INVALID"
+ end select
+ ! type(string_t), allocatable :: modules_used(:)
+ s = s // ", modules_used=["
+ do i = 1, size(source%modules_used)
+ s = s // '"' // source%modules_used(i)%s // '"'
+ if (i < size(source%modules_used)) s = s // ", "
+ end do
+ s = s // "]"
+ ! type(string_t), allocatable :: include_dependencies(:)
+ s = s // ", include_dependencies=["
+ do i = 1, size(source%include_dependencies)
+ s = s // '"' // source%include_dependencies(i)%s // '"'
+ if (i < size(source%include_dependencies)) s = s // ", "
+ end do
+ s = s // "]"
+ ! type(string_t), allocatable :: link_libraries(:)
+ s = s // ", link_libraries=["
+ do i = 1, size(source%link_libraries)
+ s = s // '"' // source%link_libraries(i)%s // '"'
+ if (i < size(source%link_libraries)) s = s // ", "
+ end do
+ s = s // "]"
+ ! integer(int64) :: digest
+ s = s // ", digest=" // str(source%digest)
+ !end type srcfile_t
+ s = s // ")"
+end function info_srcfile
+
+function info_srcfile_short(source) result(s)
+ ! Prints a shortened version of srcfile_t
+ type(srcfile_t), intent(in) :: source
+ character(:), allocatable :: s
+ integer :: i
+ s = "srcfile_t("
+ s = s // 'file_name="' // source%file_name // '"'
+ s = s // ", ...)"
+end function info_srcfile_short
+
+function info_model(model) result(s)
+ type(fpm_model_t), intent(in) :: model
+ character(:), allocatable :: s
+ integer :: i
+ !type :: fpm_model_t
+ s = "fpm_model_t("
+ ! character(:), allocatable :: package_name
+ s = s // 'package_name="' // model%package_name // '"'
+ ! type(srcfile_t), allocatable :: sources(:)
+ s = s // ", packages=["
+ do i = 1, size(model%packages)
+ s = s // info_package(model%packages(i))
+ if (i < size(model%packages)) s = s // ", "
+ end do
+ s = s // "]"
+ ! character(:), allocatable :: fortran_compiler
+ s = s // ', fortran_compiler="' // model%fortran_compiler // '"'
+ ! character(:), allocatable :: fortran_compile_flags
+ s = s // ', fortran_compile_flags="' // model%fortran_compile_flags // '"'
+ ! character(:), allocatable :: output_directory
+ s = s // ', output_directory="' // model%output_directory // '"'
+ ! type(string_t), allocatable :: link_libraries(:)
+ s = s // ", link_libraries=["
+ do i = 1, size(model%link_libraries)
+ s = s // '"' // model%link_libraries(i)%s // '"'
+ if (i < size(model%link_libraries)) s = s // ", "
+ end do
+ s = s // "]"
+ ! type(dependency_tree_t) :: deps
+ ! TODO: print `dependency_tree_t` properly, which should become part of the
+ ! model, not imported from another file
+ s = s // ", deps=dependency_tree_t(...)"
+ !end type fpm_model_t
+ s = s // ")"
+end function info_model
+
+subroutine show_model(model)
+ ! Prints a human readable representation of the Model
+ type(fpm_model_t), intent(in) :: model
+ print *, info_model(model)
+end subroutine show_model
+
+end module fpm_model
diff --git a/src/fpm_source_parsing.f90 b/src/fpm_source_parsing.f90
new file mode 100644
index 0000000..dd9a4c5
--- /dev/null
+++ b/src/fpm_source_parsing.f90
@@ -0,0 +1,480 @@
+!># Parsing of package source files
+!>
+!> This module exposes two functions, `[[parse_f_source]]` and `[[parse_c_source]]`,
+!> which perform a rudimentary parsing of fortran and c source files
+!> in order to extract information required for module dependency tracking.
+!>
+!> Both functions additionally calculate and store a file digest (hash) which
+!> is used by the backend ([[fpm_backend]]) to skip compilation of unmodified sources.
+!>
+!> Both functions return an instance of the [[srcfile_t]] type.
+!>
+!> For more information, please read the documentation for each function:
+!>
+!> - `[[parse_f_source]]`
+!> - `[[parse_c_source]]`
+!>
+module fpm_source_parsing
+use fpm_error, only: error_t, file_parse_error, fatal_error
+use fpm_strings, only: string_t, string_cat, len_trim, split, lower, str_ends_with, fnv_1a
+use fpm_model, only: srcfile_t, &
+ FPM_UNIT_UNKNOWN, FPM_UNIT_PROGRAM, FPM_UNIT_MODULE, &
+ FPM_UNIT_SUBMODULE, FPM_UNIT_SUBPROGRAM, &
+ FPM_UNIT_CSOURCE, FPM_UNIT_CHEADER, FPM_SCOPE_UNKNOWN, &
+ FPM_SCOPE_LIB, FPM_SCOPE_DEP, FPM_SCOPE_APP, FPM_SCOPE_TEST
+use fpm_filesystem, only: read_lines
+implicit none
+
+private
+public :: parse_f_source, parse_c_source
+
+character(15), parameter :: INTRINSIC_MODULE_NAMES(*) = &
+ ['iso_c_binding ', &
+ 'iso_fortran_env', &
+ 'ieee_arithmetic', &
+ 'ieee_exceptions', &
+ 'ieee_features ', &
+ 'omp_lib ']
+
+contains
+
+!> Parsing of free-form fortran source files
+!>
+!> The following statements are recognised and parsed:
+!>
+!> - `Module`/`submodule`/`program` declaration
+!> - Module `use` statement
+!> - `include` statement
+!>
+!> @note Intrinsic modules used by sources are not listed in
+!> the `modules_used` field of source objects.
+!>
+!> @note Submodules are treated as normal modules which `use` their
+!> corresponding parent modules.
+!>
+!>### Parsing limitations
+!>
+!> __Statements must not continued onto another line
+!> except for an `only:` list in the `use` statement.__
+!>
+!> This is supported:
+!>
+!>```fortran
+!> use my_module, only: &
+!> my_var, my_function, my_subroutine
+!>```
+!>
+!> This is __NOT supported:__
+!>
+!>```fortran
+!> use &
+!> my_module
+!>```
+!>
+function parse_f_source(f_filename,error) result(f_source)
+ character(*), intent(in) :: f_filename
+ type(srcfile_t) :: f_source
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: stat
+ integer :: fh, n_use, n_include, n_mod, i, j, ic, pass
+ type(string_t), allocatable :: file_lines(:)
+ character(:), allocatable :: temp_string, mod_name
+
+ f_source%file_name = f_filename
+
+ open(newunit=fh,file=f_filename,status='old')
+ file_lines = read_lines(fh)
+ close(fh)
+
+ ! Ignore empty files, returned as FPM_UNIT_UNKNOW
+ if (len_trim(file_lines) < 1) return
+
+ f_source%digest = fnv_1a(file_lines)
+
+ do pass = 1,2
+ n_use = 0
+ n_include = 0
+ n_mod = 0
+ file_loop: do i=1,size(file_lines)
+
+ ! Skip lines that are continued: not statements
+ if (i > 1) then
+ ic = index(file_lines(i-1)%s,'!')
+ if (ic < 1) then
+ ic = len(file_lines(i-1)%s)
+ end if
+ temp_string = trim(file_lines(i-1)%s(1:ic))
+ if (len(temp_string) > 0 .and. index(temp_string,'&') == len(temp_string)) then
+ cycle
+ end if
+ end if
+
+ ! Process 'USE' statements
+ if (index(adjustl(lower(file_lines(i)%s)),'use ') == 1 .or. &
+ index(adjustl(lower(file_lines(i)%s)),'use::') == 1) then
+
+ if (index(file_lines(i)%s,'::') > 0) then
+
+ temp_string = split_n(file_lines(i)%s,delims=':',n=2,stat=stat)
+ if (stat /= 0) then
+ call file_parse_error(error,f_filename, &
+ 'unable to find used module name',i, &
+ file_lines(i)%s,index(file_lines(i)%s,'::'))
+ return
+ end if
+
+ mod_name = split_n(temp_string,delims=' ,',n=1,stat=stat)
+ if (stat /= 0) then
+ call file_parse_error(error,f_filename, &
+ 'unable to find used module name',i, &
+ file_lines(i)%s)
+ return
+ end if
+ mod_name = lower(mod_name)
+
+ else
+
+ mod_name = split_n(file_lines(i)%s,n=2,delims=' ,',stat=stat)
+ if (stat /= 0) then
+ call file_parse_error(error,f_filename, &
+ 'unable to find used module name',i, &
+ file_lines(i)%s)
+ return
+ end if
+ mod_name = lower(mod_name)
+
+ end if
+
+ if (.not.validate_name(mod_name)) then
+ cycle
+ end if
+
+ if (any([(index(mod_name,trim(INTRINSIC_MODULE_NAMES(j)))>0, &
+ j=1,size(INTRINSIC_MODULE_NAMES))])) then
+ cycle
+ end if
+
+ n_use = n_use + 1
+
+ if (pass == 2) then
+
+ f_source%modules_used(n_use)%s = mod_name
+
+ end if
+
+ end if
+
+ ! Process 'INCLUDE' statements
+ ic = index(adjustl(lower(file_lines(i)%s)),'include')
+ if ( ic == 1 ) then
+ ic = index(lower(file_lines(i)%s),'include')
+ if (index(adjustl(file_lines(i)%s(ic+7:)),'"') == 1 .or. &
+ index(adjustl(file_lines(i)%s(ic+7:)),"'") == 1 ) then
+
+
+ n_include = n_include + 1
+
+ if (pass == 2) then
+ f_source%include_dependencies(n_include)%s = &
+ & split_n(file_lines(i)%s,n=2,delims="'"//'"',stat=stat)
+ if (stat /= 0) then
+ call file_parse_error(error,f_filename, &
+ 'unable to find include file name',i, &
+ file_lines(i)%s)
+ return
+ end if
+ end if
+ end if
+ end if
+
+ ! Extract name of module if is module
+ if (index(adjustl(lower(file_lines(i)%s)),'module ') == 1) then
+
+ mod_name = lower(split_n(file_lines(i)%s,n=2,delims=' ',stat=stat))
+ if (stat /= 0) then
+ call file_parse_error(error,f_filename, &
+ 'unable to find module name',i, &
+ file_lines(i)%s)
+ return
+ end if
+
+ if (mod_name == 'procedure' .or. &
+ mod_name == 'subroutine' .or. &
+ mod_name == 'function' .or. &
+ scan(mod_name,'=(')>0 ) then
+ ! Ignore these cases:
+ ! module procedure *
+ ! module function *
+ ! module subroutine *
+ ! module =*
+ ! module (i)
+ cycle
+ end if
+
+ if (.not.validate_name(mod_name)) then
+ call file_parse_error(error,f_filename, &
+ 'empty or invalid name for module',i, &
+ file_lines(i)%s, index(file_lines(i)%s,mod_name))
+ return
+ end if
+
+ n_mod = n_mod + 1
+
+ if (pass == 2) then
+ f_source%modules_provided(n_mod) = string_t(mod_name)
+ end if
+
+ f_source%unit_type = FPM_UNIT_MODULE
+
+ end if
+
+ ! Extract name of submodule if is submodule
+ if (index(adjustl(lower(file_lines(i)%s)),'submodule') == 1) then
+
+ mod_name = split_n(file_lines(i)%s,n=3,delims='()',stat=stat)
+ if (stat /= 0) then
+ call file_parse_error(error,f_filename, &
+ 'unable to get submodule name',i, &
+ file_lines(i)%s)
+ return
+ end if
+ if (.not.validate_name(mod_name)) then
+ call file_parse_error(error,f_filename, &
+ 'empty or invalid name for submodule',i, &
+ file_lines(i)%s, index(file_lines(i)%s,mod_name))
+ return
+ end if
+
+ n_mod = n_mod + 1
+
+ temp_string = split_n(file_lines(i)%s,n=2,delims='()',stat=stat)
+ if (stat /= 0) then
+ call file_parse_error(error,f_filename, &
+ 'unable to get submodule ancestry',i, &
+ file_lines(i)%s)
+ return
+ end if
+
+ f_source%unit_type = FPM_UNIT_SUBMODULE
+
+ n_use = n_use + 1
+
+ if (pass == 2) then
+
+ if (index(temp_string,':') > 0) then
+
+ temp_string = temp_string(index(temp_string,':')+1:)
+
+ end if
+
+ if (.not.validate_name(temp_string)) then
+ call file_parse_error(error,f_filename, &
+ 'empty or invalid name for submodule parent',i, &
+ file_lines(i)%s, index(file_lines(i)%s,temp_string))
+ return
+ end if
+
+ f_source%modules_used(n_use)%s = lower(temp_string)
+
+ f_source%modules_provided(n_mod)%s = lower(mod_name)
+
+ end if
+
+ end if
+
+ ! Detect if contains a program
+ ! (no modules allowed after program def)
+ if (index(adjustl(lower(file_lines(i)%s)),'program ') == 1) then
+
+ temp_string = lower(split_n(file_lines(i)%s,n=2,delims=' ',stat=stat))
+ if (stat == 0) then
+
+ if (scan(temp_string,'=(')>0 ) then
+ ! Ignore:
+ ! program =*
+ ! program (i) =*
+ cycle
+ end if
+
+ end if
+
+ f_source%unit_type = FPM_UNIT_PROGRAM
+
+ end if
+
+ end do file_loop
+
+ ! Default to subprogram unit type
+ if (f_source%unit_type == FPM_UNIT_UNKNOWN) then
+ f_source%unit_type = FPM_UNIT_SUBPROGRAM
+ end if
+
+ if (pass == 1) then
+ allocate(f_source%modules_used(n_use))
+ allocate(f_source%include_dependencies(n_include))
+ allocate(f_source%modules_provided(n_mod))
+ end if
+
+ end do
+
+ contains
+
+ function validate_name(name) result(valid)
+ character(*), intent(in) :: name
+ logical :: valid
+
+ integer :: i
+
+ if (len_trim(name) < 1) then
+ valid = .false.
+ return
+ end if
+
+ if (lower(name(1:1)) < 'a' .or. &
+ lower(name(1:1)) > 'z') then
+
+ valid = .false.
+ return
+ end if
+
+ do i=1,len(name)
+
+ if (.not.( &
+ (name(i:i) >= '0' .and. name(i:i) <= '9').or. &
+ (lower(name(i:i)) >= 'a' .and. lower(name(i:i)) <= 'z').or. &
+ name(i:i) == '_') ) then
+
+ valid = .false.
+ return
+ end if
+
+ end do
+
+ valid = .true.
+ return
+
+ end function validate_name
+
+end function parse_f_source
+
+
+!> Parsing of c source files
+!>
+!> The following statements are recognised and parsed:
+!>
+!> - `#include` preprocessor statement
+!>
+function parse_c_source(c_filename,error) result(c_source)
+ character(*), intent(in) :: c_filename
+ type(srcfile_t) :: c_source
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: fh, n_include, i, pass, stat
+ type(string_t), allocatable :: file_lines(:)
+
+ c_source%file_name = c_filename
+
+ if (str_ends_with(lower(c_filename), ".c")) then
+
+ c_source%unit_type = FPM_UNIT_CSOURCE
+
+ elseif (str_ends_with(lower(c_filename), ".h")) then
+
+ c_source%unit_type = FPM_UNIT_CHEADER
+
+ end if
+
+ allocate(c_source%modules_used(0))
+ allocate(c_source%modules_provided(0))
+
+ open(newunit=fh,file=c_filename,status='old')
+ file_lines = read_lines(fh)
+ close(fh)
+
+ ! Ignore empty files, returned as FPM_UNIT_UNKNOW
+ if (len_trim(file_lines) < 1) then
+ c_source%unit_type = FPM_UNIT_UNKNOWN
+ return
+ end if
+
+ c_source%digest = fnv_1a(file_lines)
+
+ do pass = 1,2
+ n_include = 0
+ file_loop: do i=1,size(file_lines)
+
+ ! Process 'INCLUDE' statements
+ if (index(adjustl(lower(file_lines(i)%s)),'#include') == 1 .and. &
+ index(file_lines(i)%s,'"') > 0) then
+
+ n_include = n_include + 1
+
+ if (pass == 2) then
+
+ c_source%include_dependencies(n_include)%s = &
+ & split_n(file_lines(i)%s,n=2,delims='"',stat=stat)
+ if (stat /= 0) then
+ call file_parse_error(error,c_filename, &
+ 'unable to get c include file',i, &
+ file_lines(i)%s,index(file_lines(i)%s,'"'))
+ return
+ end if
+
+ end if
+
+ end if
+
+ end do file_loop
+
+ if (pass == 1) then
+ allocate(c_source%include_dependencies(n_include))
+ end if
+
+ end do
+
+end function parse_c_source
+
+!> Split a string on one or more delimeters
+!> and return the nth substring if it exists
+!>
+!> n=0 will return the last item
+!> n=-1 will return the penultimate item etc.
+!>
+!> stat = 1 on return if the index
+!> is not found
+!>
+function split_n(string,delims,n,stat) result(substring)
+
+ character(*), intent(in) :: string
+ character(*), intent(in) :: delims
+ integer, intent(in) :: n
+ integer, intent(out) :: stat
+ character(:), allocatable :: substring
+
+ integer :: i
+ character(:), allocatable :: string_parts(:)
+
+ call split(string,string_parts,delims)
+
+ if (n<1) then
+ i = size(string_parts) + n
+ if (i < 1) then
+ stat = 1
+ return
+ end if
+ else
+ i = n
+ end if
+
+ if (i>size(string_parts)) then
+ stat = 1
+ return
+ end if
+
+ substring = trim(adjustl(string_parts(i)))
+ stat = 0
+
+end function split_n
+
+end module fpm_source_parsing
diff --git a/src/fpm_sources.f90 b/src/fpm_sources.f90
new file mode 100644
index 0000000..c781535
--- /dev/null
+++ b/src/fpm_sources.f90
@@ -0,0 +1,220 @@
+!># Discovery of sources
+!>
+!> This module implements subroutines for building a list of
+!> `[[srcfile_t]]` objects by looking for source files in the filesystem.
+!>
+module fpm_sources
+use fpm_error, only: error_t
+use fpm_model, only: srcfile_t, FPM_UNIT_PROGRAM
+use fpm_filesystem, only: basename, canon_path, dirname, join_path, list_files
+use fpm_strings, only: lower, str_ends_with, string_t, operator(.in.)
+use fpm_source_parsing, only: parse_f_source, parse_c_source
+use fpm_manifest_executable, only: executable_config_t
+implicit none
+
+private
+public :: add_sources_from_dir, add_executable_sources
+
+character(4), parameter :: fortran_suffixes(2) = [".f90", &
+ ".f "]
+
+contains
+
+!> Wrapper to source parsing routines.
+!> Selects parsing routine based on source file name extension
+function parse_source(source_file_path,error) result(source)
+ character(*), intent(in) :: source_file_path
+ type(error_t), allocatable, intent(out) :: error
+ type(srcfile_t) :: source
+
+ if (str_ends_with(lower(source_file_path), fortran_suffixes)) then
+
+ source = parse_f_source(source_file_path, error)
+
+ if (source%unit_type == FPM_UNIT_PROGRAM) then
+ source%exe_name = basename(source_file_path,suffix=.false.)
+ end if
+
+ else if (str_ends_with(lower(source_file_path), [".c", ".h"])) then
+
+ source = parse_c_source(source_file_path,error)
+
+ end if
+
+ if (allocated(error)) then
+ return
+ end if
+
+end function parse_source
+
+!> Add to `sources` by looking for source files in `directory`
+subroutine add_sources_from_dir(sources,directory,scope,with_executables,recurse,error)
+ !> List of `[[srcfile_t]]` objects to append to. Allocated if not allocated
+ type(srcfile_t), allocatable, intent(inout), target :: sources(:)
+ !> Directory in which to search for source files
+ character(*), intent(in) :: directory
+ !> Scope to apply to the discovered sources, see [[fpm_model]] for enumeration
+ integer, intent(in) :: scope
+ !> Executable sources (fortran `program`s) are ignored unless `with_executables=.true.`
+ logical, intent(in), optional :: with_executables
+ !> Whether to recursively search subdirectories, default is `.true.`
+ logical, intent(in), optional :: recurse
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: i
+ logical, allocatable :: is_source(:), exclude_source(:)
+ type(string_t), allocatable :: file_names(:)
+ type(string_t), allocatable :: src_file_names(:)
+ type(string_t), allocatable :: existing_src_files(:)
+ type(srcfile_t), allocatable :: dir_sources(:)
+
+ ! Scan directory for sources
+ call list_files(directory, file_names,recurse=merge(recurse,.true.,present(recurse)))
+
+ if (allocated(sources)) then
+ allocate(existing_src_files(size(sources)))
+ do i=1,size(sources)
+ existing_src_files(i)%s = canon_path(sources(i)%file_name)
+ end do
+ else
+ allocate(existing_src_files(0))
+ end if
+
+ is_source = [(.not.(canon_path(file_names(i)%s) .in. existing_src_files) .and. &
+ (str_ends_with(lower(file_names(i)%s), fortran_suffixes) .or. &
+ str_ends_with(lower(file_names(i)%s),[".c",".h"]) ),i=1,size(file_names))]
+ src_file_names = pack(file_names,is_source)
+
+ allocate(dir_sources(size(src_file_names)))
+ allocate(exclude_source(size(src_file_names)))
+
+ do i = 1, size(src_file_names)
+
+ dir_sources(i) = parse_source(src_file_names(i)%s,error)
+ if (allocated(error)) return
+
+ dir_sources(i)%unit_scope = scope
+
+ ! Exclude executables unless specified otherwise
+ exclude_source(i) = (dir_sources(i)%unit_type == FPM_UNIT_PROGRAM)
+ if (dir_sources(i)%unit_type == FPM_UNIT_PROGRAM .and. &
+ & present(with_executables)) then
+ if (with_executables) then
+
+ exclude_source(i) = .false.
+
+ end if
+ end if
+
+ end do
+
+ if (.not.allocated(sources)) then
+ sources = pack(dir_sources,.not.exclude_source)
+ else
+ sources = [sources, pack(dir_sources,.not.exclude_source)]
+ end if
+
+end subroutine add_sources_from_dir
+
+
+!> Add to `sources` using the executable and test entries in the manifest and
+!> applies any executable-specific overrides such as `executable%name`.
+!> Adds all sources (including modules) from each `executable%source_dir`
+subroutine add_executable_sources(sources,executables,scope,auto_discover,error)
+ !> List of `[[srcfile_t]]` objects to append to. Allocated if not allocated
+ type(srcfile_t), allocatable, intent(inout), target :: sources(:)
+ !> List of `[[executable_config_t]]` entries from manifest
+ class(executable_config_t), intent(in) :: executables(:)
+ !> Scope to apply to the discovered sources: either `FPM_SCOPE_APP` or `FPM_SCOPE_TEST`, see [[fpm_model]]
+ integer, intent(in) :: scope
+ !> If `.false.` only executables and tests specified in the manifest are added to `sources`
+ logical, intent(in) :: auto_discover
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: i, j
+
+ type(string_t), allocatable :: exe_dirs(:)
+ type(srcfile_t) :: exe_source
+
+ call get_executable_source_dirs(exe_dirs,executables)
+
+ do i=1,size(exe_dirs)
+ call add_sources_from_dir(sources,exe_dirs(i)%s, scope, &
+ with_executables=auto_discover, recurse=.false., error=error)
+
+ if (allocated(error)) then
+ return
+ end if
+ end do
+
+ exe_loop: do i=1,size(executables)
+
+ ! Check if executable already discovered automatically
+ ! and apply any overrides
+ do j=1,size(sources)
+
+ if (basename(sources(j)%file_name,suffix=.true.) == executables(i)%main .and.&
+ canon_path(dirname(sources(j)%file_name)) == &
+ canon_path(executables(i)%source_dir) ) then
+
+ sources(j)%exe_name = executables(i)%name
+ if (allocated(executables(i)%link)) then
+ sources(j)%link_libraries = executables(i)%link
+ end if
+ cycle exe_loop
+
+ end if
+
+ end do
+
+ ! Add if not already discovered (auto_discovery off)
+ exe_source = parse_source(join_path(executables(i)%source_dir,executables(i)%main),error)
+ exe_source%exe_name = executables(i)%name
+ if (allocated(executables(i)%link)) then
+ exe_source%link_libraries = executables(i)%link
+ end if
+ exe_source%unit_scope = scope
+
+ if (allocated(error)) return
+
+ if (.not.allocated(sources)) then
+ sources = [exe_source]
+ else
+ sources = [sources, exe_source]
+ end if
+
+ end do exe_loop
+
+end subroutine add_executable_sources
+
+!> Build a list of unique source directories
+!> from executables specified in manifest
+subroutine get_executable_source_dirs(exe_dirs,executables)
+ type(string_t), allocatable, intent(inout) :: exe_dirs(:)
+ class(executable_config_t), intent(in) :: executables(:)
+
+ type(string_t) :: dirs_temp(size(executables))
+
+ integer :: i, n
+
+ n = 0
+ do i=1,size(executables)
+ if (.not.(executables(i)%source_dir .in. dirs_temp)) then
+
+ n = n + 1
+ dirs_temp(n)%s = executables(i)%source_dir
+
+ end if
+ end do
+
+ if (.not.allocated(exe_dirs)) then
+ exe_dirs = dirs_temp(1:n)
+ else
+ exe_dirs = [exe_dirs,dirs_temp(1:n)]
+ end if
+
+end subroutine get_executable_source_dirs
+
+end module fpm_sources
diff --git a/src/fpm_strings.f90 b/src/fpm_strings.f90
new file mode 100644
index 0000000..3d7d7b1
--- /dev/null
+++ b/src/fpm_strings.f90
@@ -0,0 +1,924 @@
+!> This module defines general procedures for **string operations** for both CHARACTER and
+!! TYPE(STRING_T) variables
+!
+!>## general routines for performing __string operations__
+!!
+!!### Types
+!! - **TYPE(STRING_T)** define a type to contain strings of variable length
+!!### Type Conversions
+!! - [[F_STRING]] return Fortran **CHARACTER** variable when given a C-like array of
+!! single characters terminated with a C_NULL_CHAR **CHARACTER**
+!! - [[STR]] Converts **INTEGER** or** LOGICAL** to **CHARACTER** string
+!!### Case
+!! - [[LOWER]] Changes a string to lowercase over optional specified column range
+!!### Parsing and joining
+!! - [[SPLIT]] parse string on delimiter characters and store tokens into an allocatable array
+!! - [[STRING_CAT]] Concatenate an array of **type(string_t)** into a single **CHARACTER** variable
+!! - [[JOIN]] append an array of **CHARACTER** variables into a single **CHARACTER** variable
+!!### Testing
+!! - [[STR_ENDS_WITH]] test if a **CHARACTER** string or array ends with a specified suffix
+!! - [[STRING_ARRAY_CONTAINS]] Check if array of **TYPE(STRING_T)** matches a particular **CHARACTER** string
+!! - **OPERATOR(.IN.)** Check if array of **TYPE(STRING_T)** matches a particular **CHARACTER** string
+!! - [[GLOB]] function compares text strings, one of which can have wildcards ('*' or '?').
+!!### Miscellaneous
+!! - [[LEN_TRIM]] Determine total trimmed length of **STRING_T** array
+!! - [[FNV_1A]] Hash a **CHARACTER(*)** string of default kind or a **TYPE(STRING_T)** array
+!! - [[REPLACE]] Returns string with characters in charset replaced with target_char.
+!! - [[RESIZE]] increase the size of a **TYPE(STRING_T)** array by N elements
+!!
+
+module fpm_strings
+use iso_fortran_env, only: int64
+implicit none
+
+private
+public :: f_string, lower, split, str_ends_with, string_t
+public :: string_array_contains, string_cat, len_trim, operator(.in.), fnv_1a
+public :: replace, resize, str, join, glob
+
+type string_t
+ character(len=:), allocatable :: s
+end type
+
+interface len_trim
+ module procedure :: string_len_trim
+end interface len_trim
+
+interface resize
+ module procedure :: resize_string
+end interface
+
+interface operator(.in.)
+ module procedure string_array_contains
+end interface
+
+interface fnv_1a
+ procedure :: fnv_1a_char
+ procedure :: fnv_1a_string_t
+end interface fnv_1a
+
+interface str_ends_with
+ procedure :: str_ends_with_str
+ procedure :: str_ends_with_any
+end interface str_ends_with
+
+interface str
+ module procedure str_int, str_int64, str_logical
+end interface
+
+interface string_t
+ module procedure new_string_t
+end interface string_t
+
+contains
+
+!> test if a CHARACTER string ends with a specified suffix
+pure logical function str_ends_with_str(s, e) result(r)
+ character(*), intent(in) :: s, e
+ integer :: n1, n2
+ n1 = len(s)-len(e)+1
+ n2 = len(s)
+ if (n1 < 1) then
+ r = .false.
+ else
+ r = (s(n1:n2) == e)
+ end if
+end function str_ends_with_str
+
+!> test if a CHARACTER string ends with any of an array of suffixs
+pure logical function str_ends_with_any(s, e) result(r)
+ character(*), intent(in) :: s
+ character(*), intent(in) :: e(:)
+
+ integer :: i
+
+ r = .true.
+ do i=1,size(e)
+
+ if (str_ends_with(s,trim(e(i)))) return
+
+ end do
+ r = .false.
+
+end function str_ends_with_any
+
+!> return Fortran character variable when given a C-like array of
+!! single characters terminated with a C_NULL_CHAR character
+function f_string(c_string)
+ use iso_c_binding
+ character(len=1), intent(in) :: c_string(:)
+ character(:), allocatable :: f_string
+
+ integer :: i, n
+
+ i = 0
+ do while(c_string(i+1) /= C_NULL_CHAR)
+ i = i + 1
+ end do
+ n = i
+
+ allocate(character(n) :: f_string)
+ do i=1,n
+ f_string(i:i) = c_string(i)
+ end do
+
+end function f_string
+
+
+!> Hash a character(*) string of default kind
+pure function fnv_1a_char(input, seed) result(hash)
+ character(*), intent(in) :: input
+ integer(int64), intent(in), optional :: seed
+ integer(int64) :: hash
+
+ integer :: i
+ integer(int64), parameter :: FNV_OFFSET_32 = 2166136261_int64
+ integer(int64), parameter :: FNV_PRIME_32 = 16777619_int64
+
+ if (present(seed)) then
+ hash = seed
+ else
+ hash = FNV_OFFSET_32
+ end if
+
+ do i=1,len(input)
+ hash = ieor(hash,iachar(input(i:i),int64)) * FNV_PRIME_32
+ end do
+
+end function fnv_1a_char
+
+
+!> Hash a string_t array of default kind
+pure function fnv_1a_string_t(input, seed) result(hash)
+ type(string_t), intent(in) :: input(:)
+ integer(int64), intent(in), optional :: seed
+ integer(int64) :: hash
+
+ integer :: i
+
+ hash = fnv_1a(input(1)%s,seed)
+
+ do i=2,size(input)
+ hash = fnv_1a(input(i)%s,hash)
+ end do
+
+end function fnv_1a_string_t
+
+
+ !>Author: John S. Urban
+ !!License: Public Domain
+ !! Changes a string to lowercase over optional specified column range
+elemental pure function lower(str,begin,end) result (string)
+
+ character(*), intent(In) :: str
+ character(len(str)) :: string
+ integer,intent(in),optional :: begin, end
+ integer :: i
+ integer :: ibegin, iend
+ string = str
+
+ ibegin = 1
+ if (present(begin))then
+ ibegin = max(ibegin,begin)
+ endif
+
+ iend = len_trim(str)
+ if (present(end))then
+ iend= min(iend,end)
+ endif
+
+ do i = ibegin, iend ! step thru each letter in the string in specified range
+ select case (str(i:i))
+ case ('A':'Z')
+ string(i:i) = char(iachar(str(i:i))+32) ! change letter to miniscule
+ case default
+ end select
+ end do
+
+end function lower
+
+!> Helper function to generate a new string_t instance
+!> (Required due to the allocatable component)
+function new_string_t(s) result(string)
+ character(*), intent(in) :: s
+ type(string_t) :: string
+
+ string%s = s
+
+end function new_string_t
+
+!> Check if array of TYPE(STRING_T) matches a particular CHARACTER string
+!!
+logical function string_array_contains(search_string,array)
+ character(*), intent(in) :: search_string
+ type(string_t), intent(in) :: array(:)
+
+ integer :: i
+
+ string_array_contains = any([(array(i)%s==search_string, &
+ i=1,size(array))])
+
+end function string_array_contains
+
+!> Concatenate an array of type(string_t) into
+!> a single CHARACTER variable
+function string_cat(strings,delim) result(cat)
+ type(string_t), intent(in) :: strings(:)
+ character(*), intent(in), optional :: delim
+ character(:), allocatable :: cat
+
+ integer :: i
+ character(:), allocatable :: delim_str
+
+ if (size(strings) < 1) then
+ cat = ''
+ return
+ end if
+
+ if (present(delim)) then
+ delim_str = delim
+ else
+ delim_str = ''
+ end if
+
+ cat = strings(1)%s
+ do i=2,size(strings)
+
+ cat = cat//delim_str//strings(i)%s
+
+ end do
+
+end function string_cat
+
+!> Determine total trimmed length of `string_t` array
+pure function string_len_trim(strings) result(n)
+ type(string_t), intent(in) :: strings(:)
+ integer :: i, n
+
+ n = 0
+ do i=1,size(strings)
+ n = n + len_trim(strings(i)%s)
+ end do
+
+end function string_len_trim
+
+!>Author: John S. Urban
+!!License: Public Domain
+!! parse string on delimiter characters and store tokens into an allocatable array
+subroutine split(input_line,array,delimiters,order,nulls)
+ !! given a line of structure " par1 par2 par3 ... parn " store each par(n) into a separate variable in array.
+ !!
+ !! * by default adjacent delimiters in the input string do not create an empty string in the output array
+ !! * no quoting of delimiters is supported
+ character(len=*),intent(in) :: input_line !! input string to tokenize
+ character(len=*),optional,intent(in) :: delimiters !! list of delimiter characters
+ character(len=*),optional,intent(in) :: order !! order of output array sequential|[reverse|right]
+ character(len=*),optional,intent(in) :: nulls !! return strings composed of delimiters or not ignore|return|ignoreend
+ character(len=:),allocatable,intent(out) :: array(:) !! output array of tokens
+
+ integer :: n ! max number of strings INPUT_LINE could split into if all delimiter
+ integer,allocatable :: ibegin(:) ! positions in input string where tokens start
+ integer,allocatable :: iterm(:) ! positions in input string where tokens end
+ character(len=:),allocatable :: dlim ! string containing delimiter characters
+ character(len=:),allocatable :: ordr ! string containing order keyword
+ character(len=:),allocatable :: nlls ! string containing nulls keyword
+ integer :: ii,iiii ! loop parameters used to control print order
+ integer :: icount ! number of tokens found
+ integer :: ilen ! length of input string with trailing spaces trimmed
+ integer :: i10,i20,i30 ! loop counters
+ integer :: icol ! pointer into input string as it is being parsed
+ integer :: idlim ! number of delimiter characters
+ integer :: ifound ! where next delimiter character is found in remaining input string data
+ integer :: inotnull ! count strings not composed of delimiters
+ integer :: ireturn ! number of tokens returned
+ integer :: imax ! length of longest token
+
+ ! decide on value for optional DELIMITERS parameter
+ if (present(delimiters)) then ! optional delimiter list was present
+ if(delimiters.ne.'')then ! if DELIMITERS was specified and not null use it
+ dlim=delimiters
+ else ! DELIMITERS was specified on call as empty string
+ dlim=' '//char(9)//char(10)//char(11)//char(12)//char(13)//char(0) ! use default delimiter when not specified
+ endif
+ else ! no delimiter value was specified
+ dlim=' '//char(9)//char(10)//char(11)//char(12)//char(13)//char(0) ! use default delimiter when not specified
+ endif
+ idlim=len(dlim) ! dlim a lot of blanks on some machines if dlim is a big string
+
+ if(present(order))then; ordr=lower(adjustl(order)); else; ordr='sequential'; endif ! decide on value for optional ORDER parameter
+ if(present(nulls))then; nlls=lower(adjustl(nulls)); else; nlls='ignore' ; endif ! optional parameter
+
+ n=len(input_line)+1 ! max number of strings INPUT_LINE could split into if all delimiter
+ allocate(ibegin(n)) ! allocate enough space to hold starting location of tokens if string all tokens
+ allocate(iterm(n)) ! allocate enough space to hold ending location of tokens if string all tokens
+ ibegin(:)=1
+ iterm(:)=1
+
+ ilen=len(input_line) ! ILEN is the column position of the last non-blank character
+ icount=0 ! how many tokens found
+ inotnull=0 ! how many tokens found not composed of delimiters
+ imax=0 ! length of longest token found
+
+ select case (ilen)
+
+ case (0) ! command was totally blank
+
+ case default ! there is at least one non-delimiter in INPUT_LINE if get here
+ icol=1 ! initialize pointer into input line
+ INFINITE: do i30=1,ilen,1 ! store into each array element
+ ibegin(i30)=icol ! assume start new token on the character
+ if(index(dlim(1:idlim),input_line(icol:icol)).eq.0)then ! if current character is not a delimiter
+ iterm(i30)=ilen ! initially assume no more tokens
+ do i10=1,idlim ! search for next delimiter
+ ifound=index(input_line(ibegin(i30):ilen),dlim(i10:i10))
+ IF(ifound.gt.0)then
+ iterm(i30)=min(iterm(i30),ifound+ibegin(i30)-2)
+ endif
+ enddo
+ icol=iterm(i30)+2 ! next place to look as found end of this token
+ inotnull=inotnull+1 ! increment count of number of tokens not composed of delimiters
+ else ! character is a delimiter for a null string
+ iterm(i30)=icol-1 ! record assumed end of string. Will be less than beginning
+ icol=icol+1 ! advance pointer into input string
+ endif
+ imax=max(imax,iterm(i30)-ibegin(i30)+1)
+ icount=i30 ! increment count of number of tokens found
+ if(icol.gt.ilen)then ! no text left
+ exit INFINITE
+ endif
+ enddo INFINITE
+
+ end select
+
+ select case (trim(adjustl(nlls)))
+ case ('ignore','','ignoreend')
+ ireturn=inotnull
+ case default
+ ireturn=icount
+ end select
+ allocate(character(len=imax) :: array(ireturn)) ! allocate the array to return
+ !allocate(array(ireturn)) ! allocate the array to turn
+
+ select case (trim(adjustl(ordr))) ! decide which order to store tokens
+ case ('reverse','right') ; ii=ireturn ; iiii=-1 ! last to first
+ case default ; ii=1 ; iiii=1 ! first to last
+ end select
+
+ do i20=1,icount ! fill the array with the tokens that were found
+ if(iterm(i20).lt.ibegin(i20))then
+ select case (trim(adjustl(nlls)))
+ case ('ignore','','ignoreend')
+ case default
+ array(ii)=' '
+ ii=ii+iiii
+ end select
+ else
+ array(ii)=input_line(ibegin(i20):iterm(i20))
+ ii=ii+iiii
+ endif
+ enddo
+end subroutine split
+
+!> Returns string with characters in charset replaced with target_char.
+pure function replace(string, charset, target_char) result(res)
+ character(*), intent(in) :: string
+ character, intent(in) :: charset(:), target_char
+ character(len(string)) :: res
+ integer :: n
+ res = string
+ do n = 1, len(string)
+ if (any(string(n:n) == charset)) then
+ res(n:n) = target_char
+ end if
+ end do
+end function replace
+
+!> increase the size of a TYPE(STRING_T) array by N elements
+subroutine resize_string(list, n)
+ !> Instance of the array to be resized
+ type(string_t), allocatable, intent(inout) :: list(:)
+ !> Dimension of the final array size
+ integer, intent(in), optional :: n
+
+ type(string_t), allocatable :: tmp(:)
+ integer :: this_size, new_size, i
+ integer, parameter :: initial_size = 16
+
+ if (allocated(list)) then
+ this_size = size(list, 1)
+ call move_alloc(list, tmp)
+ else
+ this_size = initial_size
+ end if
+
+ if (present(n)) then
+ new_size = n
+ else
+ new_size = this_size + this_size/2 + 1
+ end if
+
+ allocate(list(new_size))
+
+ if (allocated(tmp)) then
+ this_size = min(size(tmp, 1), size(list, 1))
+ do i = 1, this_size
+ call move_alloc(tmp(i)%s, list(i)%s)
+ end do
+ deallocate(tmp)
+ end if
+
+end subroutine resize_string
+
+!>AUTHOR: John S. Urban
+!!LICENSE: Public Domain
+!>
+!!##NAME
+!! join(3f) - [M_strings:EDITING] append CHARACTER variable array into
+!! a single CHARACTER variable with specified separator
+!! (LICENSE:PD)
+!!
+!!##SYNOPSIS
+!!
+!! pure function join(str,sep,trm,left,right,start,end) result (string)
+!!
+!! character(len=*),intent(in) :: str(:)
+!! character(len=*),intent(in),optional :: sep
+!! logical,intent(in),optional :: trm
+!! character(len=*),intent(in),optional :: right
+!! character(len=*),intent(in),optional :: left
+!! character(len=*),intent(in),optional :: start
+!! character(len=*),intent(in),optional :: end
+!! character(len=:),allocatable :: string
+!!
+!!##DESCRIPTION
+!! JOIN(3f) appends the elements of a CHARACTER array into a single
+!! CHARACTER variable, with elements 1 to N joined from left to right.
+!! By default each element is trimmed of trailing spaces and the
+!! default separator is a null string.
+!!
+!!##OPTIONS
+!! STR(:) array of CHARACTER variables to be joined
+!! SEP separator string to place between each variable. defaults
+!! to a null string.
+!! LEFT string to place at left of each element
+!! RIGHT string to place at right of each element
+!! START prefix string
+!! END suffix string
+!! TRM option to trim each element of STR of trailing
+!! spaces. Defaults to .TRUE.
+!!
+!!##RESULT
+!! STRING CHARACTER variable composed of all of the elements of STR()
+!! appended together with the optional separator SEP placed
+!! between the elements.
+!!
+!!##EXAMPLE
+!!
+!! Sample program:
+!!
+!! program demo_join
+!! use M_strings, only: join
+!! implicit none
+!! character(len=:),allocatable :: s(:)
+!! character(len=:),allocatable :: out
+!! integer :: i
+!! s=[character(len=10) :: 'United',' we',' stand,', &
+!! & ' divided',' we fall.']
+!! out=join(s)
+!! write(*,'(a)') out
+!! write(*,'(a)') join(s,trm=.false.)
+!! write(*,'(a)') (join(s,trm=.false.,sep='|'),i=1,3)
+!! write(*,'(a)') join(s,sep='<>')
+!! write(*,'(a)') join(s,sep=';',left='[',right=']')
+!! write(*,'(a)') join(s,left='[',right=']')
+!! write(*,'(a)') join(s,left='>>')
+!! end program demo_join
+!!
+!! Expected output:
+!!
+!! United we stand, divided we fall.
+!! United we stand, divided we fall.
+!! United | we | stand, | divided | we fall.
+!! United | we | stand, | divided | we fall.
+!! United | we | stand, | divided | we fall.
+!! United<> we<> stand,<> divided<> we fall.
+!! [United];[ we];[ stand,];[ divided];[ we fall.]
+!! [United][ we][ stand,][ divided][ we fall.]
+!! >>United>> we>> stand,>> divided>> we fall.
+pure function join(str,sep,trm,left,right,start,end) result (string)
+
+! @(#)M_strings::join(3f): merge string array into a single CHARACTER value adding specified separators, caps, prefix and suffix
+
+character(len=*),intent(in) :: str(:)
+character(len=*),intent(in),optional :: sep, right, left, start, end
+logical,intent(in),optional :: trm
+character(len=:),allocatable :: sep_local, left_local, right_local
+character(len=:),allocatable :: string
+logical :: trm_local
+integer :: i
+ if(present(sep))then ; sep_local=sep ; else ; sep_local='' ; endif
+ if(present(trm))then ; trm_local=trm ; else ; trm_local=.true. ; endif
+ if(present(left))then ; left_local=left ; else ; left_local='' ; endif
+ if(present(right))then ; right_local=right ; else ; right_local='' ; endif
+ string=''
+ if(size(str).eq.0)then
+ string=string//left_local//right_local
+ else
+ do i = 1,size(str)-1
+ if(trm_local)then
+ string=string//left_local//trim(str(i))//right_local//sep_local
+ else
+ string=string//left_local//str(i)//right_local//sep_local
+ endif
+ enddo
+ if(trm_local)then
+ string=string//left_local//trim(str(i))//right_local
+ else
+ string=string//left_local//str(i)//right_local
+ endif
+ endif
+ if(present(start))string=start//string
+ if(present(end))string=string//end
+end function join
+
+!>##AUTHOR John S. Urban
+!!##LICENSE Public Domain
+!!## NAME
+!! glob(3f) - [fpm_strings:COMPARE] compare given string for match to
+!! pattern which may contain wildcard characters
+!! (LICENSE:PD)
+!!
+!!## SYNOPSIS
+!!
+!! logical function glob(string, pattern )
+!!
+!! character(len=*),intent(in) :: string
+!! character(len=*),intent(in) :: pattern
+!!
+!!## DESCRIPTION
+!! glob(3f) compares given STRING for match to PATTERN which may
+!! contain wildcard characters.
+!!
+!! In this version to get a match the entire string must be described
+!! by PATTERN. Trailing whitespace is significant, so trim the input
+!! string to have trailing whitespace ignored.
+!!
+!!## OPTIONS
+!! string the input string to test to see if it contains the pattern.
+!! pattern the following simple globbing options are available
+!!
+!! o "?" matching any one character
+!! o "*" matching zero or more characters.
+!! Do NOT use adjacent asterisks.
+!! o Both strings may have trailing spaces which
+!! are ignored.
+!! o There is no escape character, so matching strings with
+!! literal question mark and asterisk is problematic.
+!!
+!!## EXAMPLES
+!!
+!! Example program
+!!
+!! program demo_glob
+!! implicit none
+!! ! This main() routine passes a bunch of test strings
+!! ! into the above code. In performance comparison mode,
+!! ! it does that over and over. Otherwise, it does it just
+!! ! once. Either way, it outputs a passed/failed result.
+!! !
+!! integer :: nReps
+!! logical :: allpassed
+!! integer :: i
+!! allpassed = .true.
+!!
+!! nReps = 10000
+!! ! Can choose as many repetitions as you're expecting
+!! ! in the real world.
+!! nReps = 1
+!!
+!! do i=1,nReps
+!! ! Cases with repeating character sequences.
+!! allpassed=allpassed .and. test("a*abab", "a*b", .true.)
+!! !!cycle
+!! allpassed=allpassed .and. test("ab", "*?", .true.)
+!! allpassed=allpassed .and. test("abc", "*?", .true.)
+!! allpassed=allpassed .and. test("abcccd", "*ccd", .true.)
+!! allpassed=allpassed .and. test("bLah", "bLaH", .false.)
+!! allpassed=allpassed .and. test("mississippi", "*sip*", .true.)
+!! allpassed=allpassed .and. &
+!! & test("xxxx*zzzzzzzzy*f", "xxx*zzy*f", .true.)
+!! allpassed=allpassed .and. &
+!! & test("xxxx*zzzzzzzzy*f", "xxxx*zzy*fffff", .false.)
+!! allpassed=allpassed .and. &
+!! & test("mississipissippi", "*issip*ss*", .true.)
+!! allpassed=allpassed .and. &
+!! & test("xxxxzzzzzzzzyf", "xxxx*zzy*fffff", .false.)
+!! allpassed=allpassed .and. &
+!! & test("xxxxzzzzzzzzyf", "xxxx*zzy*f", .true.)
+!! allpassed=allpassed .and. test("xyxyxyzyxyz", "xy*z*xyz", .true.)
+!! allpassed=allpassed .and. test("xyxyxyxyz", "xy*xyz", .true.)
+!! allpassed=allpassed .and. test("mississippi", "mi*sip*", .true.)
+!! allpassed=allpassed .and. test("ababac", "*abac*", .true.)
+!! allpassed=allpassed .and. test("aaazz", "a*zz*", .true.)
+!! allpassed=allpassed .and. test("a12b12", "*12*23", .false.)
+!! allpassed=allpassed .and. test("a12b12", "a12b", .false.)
+!! allpassed=allpassed .and. test("a12b12", "*12*12*", .true.)
+!!
+!! ! Additional cases where the '*' char appears in the tame string.
+!! allpassed=allpassed .and. test("*", "*", .true.)
+!! allpassed=allpassed .and. test("a*r", "a*", .true.)
+!! allpassed=allpassed .and. test("a*ar", "a*aar", .false.)
+!!
+!! ! More double wildcard scenarios.
+!! allpassed=allpassed .and. test("XYXYXYZYXYz", "XY*Z*XYz", .true.)
+!! allpassed=allpassed .and. test("missisSIPpi", "*SIP*", .true.)
+!! allpassed=allpassed .and. test("mississipPI", "*issip*PI", .true.)
+!! allpassed=allpassed .and. test("xyxyxyxyz", "xy*xyz", .true.)
+!! allpassed=allpassed .and. test("miSsissippi", "mi*sip*", .true.)
+!! allpassed=allpassed .and. test("miSsissippi", "mi*Sip*", .false.)
+!! allpassed=allpassed .and. test("abAbac", "*Abac*", .true.)
+!! allpassed=allpassed .and. test("aAazz", "a*zz*", .true.)
+!! allpassed=allpassed .and. test("A12b12", "*12*23", .false.)
+!! allpassed=allpassed .and. test("a12B12", "*12*12*", .true.)
+!! allpassed=allpassed .and. test("oWn", "*oWn*", .true.)
+!!
+!! ! Completely tame (no wildcards) cases.
+!! allpassed=allpassed .and. test("bLah", "bLah", .true.)
+!!
+!! ! Simple mixed wildcard tests suggested by IBMer Marlin Deckert.
+!! allpassed=allpassed .and. test("a", "*?", .true.)
+!!
+!! ! More mixed wildcard tests including coverage for false positives.
+!! allpassed=allpassed .and. test("a", "??", .false.)
+!! allpassed=allpassed .and. test("ab", "?*?", .true.)
+!! allpassed=allpassed .and. test("ab", "*?*?*", .true.)
+!! allpassed=allpassed .and. test("abc", "?**?*?", .true.)
+!! allpassed=allpassed .and. test("abc", "?**?*&?", .false.)
+!! allpassed=allpassed .and. test("abcd", "?b*??", .true.)
+!! allpassed=allpassed .and. test("abcd", "?a*??", .false.)
+!! allpassed=allpassed .and. test("abcd", "?**?c?", .true.)
+!! allpassed=allpassed .and. test("abcd", "?**?d?", .false.)
+!! allpassed=allpassed .and. test("abcde", "?*b*?*d*?", .true.)
+!!
+!! ! Single-character-match cases.
+!! allpassed=allpassed .and. test("bLah", "bL?h", .true.)
+!! allpassed=allpassed .and. test("bLaaa", "bLa?", .false.)
+!! allpassed=allpassed .and. test("bLah", "bLa?", .true.)
+!! allpassed=allpassed .and. test("bLaH", "?Lah", .false.)
+!! allpassed=allpassed .and. test("bLaH", "?LaH", .true.)
+!!
+!! ! Many-wildcard scenarios.
+!! allpassed=allpassed .and. test(&
+!! &"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&
+!! &aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab",&
+!! &"a*a*a*a*a*a*aa*aaa*a*a*b",&
+!! &.true.)
+!! allpassed=allpassed .and. test(&
+!! &"abababababababababababababababababababaacacacacacacac&
+!! &adaeafagahaiajakalaaaaaaaaaaaaaaaaaffafagaagggagaaaaaaaab",&
+!! &"*a*b*ba*ca*a*aa*aaa*fa*ga*b*",&
+!! &.true.)
+!! allpassed=allpassed .and. test(&
+!! &"abababababababababababababababababababaacacacacacaca&
+!! &cadaeafagahaiajakalaaaaaaaaaaaaaaaaaffafagaagggagaaaaaaaab",&
+!! &"*a*b*ba*ca*a*x*aaa*fa*ga*b*",&
+!! &.false.)
+!! allpassed=allpassed .and. test(&
+!! &"abababababababababababababababababababaacacacacacacacad&
+!! &aeafagahaiajakalaaaaaaaaaaaaaaaaaffafagaagggagaaaaaaaab",&
+!! &"*a*b*ba*ca*aaaa*fa*ga*gggg*b*",&
+!! &.false.)
+!! allpassed=allpassed .and. test(&
+!! &"abababababababababababababababababababaacacacacacacacad&
+!! &aeafagahaiajakalaaaaaaaaaaaaaaaaaffafagaagggagaaaaaaaab",&
+!! &"*a*b*ba*ca*aaaa*fa*ga*ggg*b*",&
+!! &.true.)
+!! allpassed=allpassed .and. test("aaabbaabbaab", "*aabbaa*a*", .true.)
+!! allpassed=allpassed .and. &
+!! test("a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*",&
+!! &"a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*", .true.)
+!! allpassed=allpassed .and. test("aaaaaaaaaaaaaaaaa",&
+!! &"*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*", .true.)
+!! allpassed=allpassed .and. test("aaaaaaaaaaaaaaaa",&
+!! &"*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*", .false.)
+!! allpassed=allpassed .and. test(&
+!! &"abc*abcd*abcde*abcdef*abcdefg*abcdefgh*abcdefghi*abcdefghij&
+!! &*abcdefghijk*abcdefghijkl*abcdefghijklm*abcdefghijklmn",&
+!! & "abc*abc*abc*abc*abc*abc*abc*abc*abc*abc*abc*abc*abc*abc&
+!! &*abc*abc*abc*",&
+!! &.false.)
+!! allpassed=allpassed .and. test(&
+!! &"abc*abcd*abcde*abcdef*abcdefg*abcdefgh*abcdefghi*abcdefghij&
+!! &*abcdefghijk*abcdefghijkl*abcdefghijklm*abcdefghijklmn",&
+!! &"abc*abc*abc*abc*abc*abc*abc*abc*abc*abc*abc*abc*",&
+!! &.true.)
+!! allpassed=allpassed .and. test("abc*abcd*abcd*abc*abcd",&
+!! &"abc*abc*abc*abc*abc", .false.)
+!! allpassed=allpassed .and. test( "abc*abcd*abcd*abc*abcd*abcd&
+!! &*abc*abcd*abc*abc*abcd", &
+!! &"abc*abc*abc*abc*abc*abc*abc*abc*abc*abc*abcd",&
+!! &.true.)
+!! allpassed=allpassed .and. test("abc",&
+!! &"********a********b********c********", .true.)
+!! allpassed=allpassed .and.&
+!! &test("********a********b********c********", "abc", .false.)
+!! allpassed=allpassed .and. &
+!! &test("abc", "********a********b********b********", .false.)
+!! allpassed=allpassed .and. test("*abc*", "***a*b*c***", .true.)
+!!
+!! ! A case-insensitive algorithm test.
+!! ! allpassed=allpassed .and. test("mississippi", "*issip*PI", .true.)
+!! enddo
+!!
+!! if (allpassed)then
+!! write(*,'(a)')"Passed",nReps
+!! else
+!! write(*,'(a)')"Failed"
+!! endif
+!! contains
+!! ! This is a test program for wildcard matching routines.
+!! ! It can be used either to test a single routine for correctness,
+!! ! or to compare the timings of two (or more) different wildcard
+!! ! matching routines.
+!! !
+!! function test(tame, wild, bExpectedResult) result(bpassed)
+!! use fpm_strings, only : glob
+!! character(len=*) :: tame
+!! character(len=*) :: wild
+!! logical :: bExpectedResult
+!! logical :: bResult
+!! logical :: bPassed
+!! bResult = .true. ! We'll do "&=" cumulative checking.
+!! bPassed = .false. ! Assume the worst.
+!! write(*,*)repeat('=',79)
+!! bResult = glob(tame, wild) ! Call a wildcard matching routine.
+!!
+!! ! To assist correctness checking, output the two strings in any
+!! ! failing scenarios.
+!! if (bExpectedResult .eqv. bResult) then
+!! bPassed = .true.
+!! if(nReps == 1) write(*,*)"Passed match on ",tame," vs. ", wild
+!! else
+!! if(nReps == 1) write(*,*)"Failed match on ",tame," vs. ", wild
+!! endif
+!!
+!! end function test
+!! end program demo_glob
+!!
+!! Expected output
+!!
+!!
+!!## REFERENCE
+!! The article "Matching Wildcards: An Empirical Way to Tame an Algorithm"
+!! in Dr Dobb's Journal, By Kirk J. Krauss, October 07, 2014
+!!
+function glob(tame,wild)
+
+! @(#)fpm_strings::glob(3f): function compares text strings, one of which can have wildcards ('*' or '?').
+
+logical :: glob !! result of test
+character(len=*) :: tame !! A string without wildcards to compare to the globbing expression
+character(len=*) :: wild !! A (potentially) corresponding string with wildcards
+character(len=len(tame)+1) :: tametext
+character(len=len(wild)+1) :: wildtext
+character(len=1),parameter :: NULL=char(0)
+integer :: wlen
+integer :: ti, wi
+integer :: i
+character(len=:),allocatable :: tbookmark, wbookmark
+! These two values are set when we observe a wildcard character. They
+! represent the locations, in the two strings, from which we start once we've observed it.
+ tametext=tame//NULL
+ wildtext=wild//NULL
+ tbookmark = NULL
+ wbookmark = NULL
+ wlen=len(wild)
+ wi=1
+ ti=1
+ do ! Walk the text strings one character at a time.
+ if(wildtext(wi:wi) == '*')then ! How do you match a unique text string?
+ do i=wi,wlen ! Easy: unique up on it!
+ if(wildtext(wi:wi).eq.'*')then
+ wi=wi+1
+ else
+ exit
+ endif
+ enddo
+ if(wildtext(wi:wi).eq.NULL) then ! "x" matches "*"
+ glob=.true.
+ return
+ endif
+ if(wildtext(wi:wi) .ne. '?') then
+ ! Fast-forward to next possible match.
+ do while (tametext(ti:ti) .ne. wildtext(wi:wi))
+ ti=ti+1
+ if (tametext(ti:ti).eq.NULL)then
+ glob=.false.
+ return ! "x" doesn't match "*y*"
+ endif
+ enddo
+ endif
+ wbookmark = wildtext(wi:)
+ tbookmark = tametext(ti:)
+ elseif(tametext(ti:ti) .ne. wildtext(wi:wi) .and. wildtext(wi:wi) .ne. '?') then
+ ! Got a non-match. If we've set our bookmarks, back up to one or both of them and retry.
+ if(wbookmark.ne.NULL) then
+ if(wildtext(wi:).ne. wbookmark) then
+ wildtext = wbookmark;
+ wlen=len_trim(wbookmark)
+ wi=1
+ ! Don't go this far back again.
+ if (tametext(ti:ti) .ne. wildtext(wi:wi)) then
+ tbookmark=tbookmark(2:)
+ tametext = tbookmark
+ ti=1
+ cycle ! "xy" matches "*y"
+ else
+ wi=wi+1
+ endif
+ endif
+ if (tametext(ti:ti).ne.NULL) then
+ ti=ti+1
+ cycle ! "mississippi" matches "*sip*"
+ endif
+ endif
+ glob=.false.
+ return ! "xy" doesn't match "x"
+ endif
+ ti=ti+1
+ wi=wi+1
+ if (tametext(ti:ti).eq.NULL) then ! How do you match a tame text string?
+ if(wildtext(wi:wi).ne.NULL)then
+ do while (wildtext(wi:wi) == '*') ! The tame way: unique up on it!
+ wi=wi+1 ! "x" matches "x*"
+ if(wildtext(wi:wi).eq.NULL)exit
+ enddo
+ endif
+ if (wildtext(wi:wi).eq.NULL)then
+ glob=.true.
+ return ! "x" matches "x"
+ endif
+ glob=.false.
+ return ! "x" doesn't match "xy"
+ endif
+ enddo
+end function glob
+
+!> Returns the length of the string representation of 'i'
+pure integer function str_int_len(i) result(sz)
+integer, intent(in) :: i
+integer, parameter :: MAX_STR = 100
+character(MAX_STR) :: s
+! If 's' is too short (MAX_STR too small), Fortran will abort with:
+! "Fortran runtime error: End of record"
+write(s, '(i0)') i
+sz = len_trim(s)
+end function
+
+!> Converts integer "i" to string
+pure function str_int(i) result(s)
+integer, intent(in) :: i
+character(len=str_int_len(i)) :: s
+write(s, '(i0)') i
+end function
+
+!> Returns the length of the string representation of 'i'
+pure integer function str_int64_len(i) result(sz)
+integer(int64), intent(in) :: i
+integer, parameter :: MAX_STR = 100
+character(MAX_STR) :: s
+! If 's' is too short (MAX_STR too small), Fortran will abort with:
+! "Fortran runtime error: End of record"
+write(s, '(i0)') i
+sz = len_trim(s)
+end function
+
+!> Converts integer "i" to string
+pure function str_int64(i) result(s)
+integer(int64), intent(in) :: i
+character(len=str_int64_len(i)) :: s
+write(s, '(i0)') i
+end function
+
+!> Returns the length of the string representation of 'l'
+pure integer function str_logical_len(l) result(sz)
+logical, intent(in) :: l
+if (l) then
+ sz = 6
+else
+ sz = 7
+end if
+end function
+
+!> Converts logical "l" to string
+pure function str_logical(l) result(s)
+logical, intent(in) :: l
+character(len=str_logical_len(l)) :: s
+if (l) then
+ s = ".true."
+else
+ s = ".false."
+end if
+end function
+
+end module fpm_strings
diff --git a/src/fpm_targets.f90 b/src/fpm_targets.f90
new file mode 100644
index 0000000..02bb600
--- /dev/null
+++ b/src/fpm_targets.f90
@@ -0,0 +1,553 @@
+!># Build target handling
+!>
+!> This module handles the construction of the build target list
+!> from the sources list (`[[targets_from_sources]]`), the
+!> resolution of module-dependencies between build targets
+!> (`[[resolve_module_dependencies]]`), and the enumeration of
+!> objects required for link targets (`[[resolve_target_linking]]`).
+!>
+!> A build target (`[[build_target_t]]`) is a file to be generated
+!> by the backend (compilation and linking).
+!>
+!> @note The current implementation is ignorant to the existence of
+!> module files (`.mod`,`.smod`). Dependencies arising from modules
+!> are based on the corresponding object files (`.o`) only.
+!>
+!> For more information, please read the documentation for the procedures:
+!>
+!> - `[[build_target_list]]`
+!> - `[[resolve_module_dependencies]]`
+!>
+!>### Enumerations
+!>
+!> __Target type:__ `FPM_TARGET_*`
+!> Describes the type of build target — determines backend build rules
+!>
+module fpm_targets
+use iso_fortran_env, only: int64
+use fpm_error, only: error_t, fatal_error
+use fpm_model
+use fpm_environment, only: get_os_type, OS_WINDOWS
+use fpm_filesystem, only: dirname, join_path, canon_path
+use fpm_strings, only: string_t, operator(.in.), string_cat
+implicit none
+
+private
+
+public FPM_TARGET_UNKNOWN, FPM_TARGET_EXECUTABLE, &
+ FPM_TARGET_ARCHIVE, FPM_TARGET_OBJECT
+public build_target_t, build_target_ptr
+public targets_from_sources, resolve_module_dependencies
+public resolve_target_linking, add_target, add_dependency
+
+
+
+!> Target type is unknown (ignored)
+integer, parameter :: FPM_TARGET_UNKNOWN = -1
+!> Target type is executable
+integer, parameter :: FPM_TARGET_EXECUTABLE = 1
+!> Target type is library archive
+integer, parameter :: FPM_TARGET_ARCHIVE = 2
+!> Target type is compiled object
+integer, parameter :: FPM_TARGET_OBJECT = 3
+
+
+!> Wrapper type for constructing arrays of `[[build_target_t]]` pointers
+type build_target_ptr
+
+ type(build_target_t), pointer :: ptr => null()
+
+end type build_target_ptr
+
+
+!> Type describing a generated build target
+type build_target_t
+
+ !> File path of build target object relative to cwd
+ character(:), allocatable :: output_file
+
+ !> Primary source for this build target
+ type(srcfile_t), allocatable :: source
+
+ !> Resolved build dependencies
+ type(build_target_ptr), allocatable :: dependencies(:)
+
+ !> Target type
+ integer :: target_type = FPM_TARGET_UNKNOWN
+
+ !> Native libraries to link against
+ type(string_t), allocatable :: link_libraries(:)
+
+ !> Objects needed to link this target
+ type(string_t), allocatable :: link_objects(:)
+
+ !> Link flags for this build target
+ character(:), allocatable :: link_flags
+
+ !> Compile flags for this build target
+ character(:), allocatable :: compile_flags
+
+ !> Flag set when first visited to check for circular dependencies
+ logical :: touched = .false.
+
+ !> Flag set if build target is sorted for building
+ logical :: sorted = .false.
+
+ !> Flag set if build target will be skipped (not built)
+ logical :: skip = .false.
+
+ !> Targets in the same schedule group are guaranteed to be independent
+ integer :: schedule = -1
+
+ !> Previous source file hash
+ integer(int64), allocatable :: digest_cached
+
+end type build_target_t
+
+
+contains
+
+!> High-level wrapper to generate build target information
+subroutine targets_from_sources(targets,model,error)
+
+ !> The generated list of build targets
+ type(build_target_ptr), intent(out), allocatable :: targets(:)
+
+ !> The package model from which to construct the target list
+ type(fpm_model_t), intent(inout), target :: model
+
+ !> Error structure
+ type(error_t), intent(out), allocatable :: error
+
+ call build_target_list(targets,model)
+
+ call resolve_module_dependencies(targets,error)
+ if (allocated(error)) return
+
+ call resolve_target_linking(targets,model)
+
+end subroutine targets_from_sources
+
+
+!> Constructs a list of build targets from a list of source files
+!>
+!>### Source-target mapping
+!>
+!> One compiled object target (`FPM_TARGET_OBJECT`) is generated for each
+!> non-executable source file (`FPM_UNIT_MODULE`,`FPM_UNIT_SUBMODULE`,
+!> `FPM_UNIT_SUBPROGRAM`,`FPM_UNIT_CSOURCE`).
+!>
+!> If any source file has scope `FPM_SCOPE_LIB` (*i.e.* there are library sources)
+!> then the first target in the target list will be a library archive target
+!> (`FPM_TARGET_ARCHIVE`). The archive target will have a dependency on every
+!> compiled object target corresponding to a library source file.
+!>
+!> One compiled object target (`FPM_TARGET_OBJECT`) and one executable target (`FPM_TARGET_EXECUTABLE`) is
+!> generated for each exectuable source file (`FPM_UNIT_PROGRAM`). The exectuble target
+!> always has a dependency on the corresponding compiled object target. If there
+!> is a library, then the executable target has an additional dependency on the library
+!> archive target.
+!>
+subroutine build_target_list(targets,model)
+
+ !> The generated list of build targets
+ type(build_target_ptr), intent(out), allocatable :: targets(:)
+
+ !> The package model from which to construct the target list
+ type(fpm_model_t), intent(inout), target :: model
+
+ integer :: i, j, n_source
+ character(:), allocatable :: xsuffix, exe_dir
+ type(build_target_t), pointer :: dep
+ logical :: with_lib
+
+ ! Check for empty build (e.g. header-only lib)
+ n_source = sum([(size(model%packages(j)%sources), &
+ j=1,size(model%packages))])
+
+ if (n_source < 1) then
+ allocate(targets(0))
+ return
+ end if
+
+ if (get_os_type() == OS_WINDOWS) then
+ xsuffix = '.exe'
+ else
+ xsuffix = ''
+ end if
+
+ with_lib = any([((model%packages(j)%sources(i)%unit_scope == FPM_SCOPE_LIB, &
+ i=1,size(model%packages(j)%sources)), &
+ j=1,size(model%packages))])
+
+ if (with_lib) call add_target(targets,type = FPM_TARGET_ARCHIVE,&
+ output_file = join_path(model%output_directory,&
+ model%package_name,'lib'//model%package_name//'.a'))
+
+ do j=1,size(model%packages)
+
+ associate(sources=>model%packages(j)%sources)
+
+ do i=1,size(sources)
+
+ select case (sources(i)%unit_type)
+ case (FPM_UNIT_MODULE,FPM_UNIT_SUBMODULE,FPM_UNIT_SUBPROGRAM,FPM_UNIT_CSOURCE)
+
+ call add_target(targets,source = sources(i), &
+ type = FPM_TARGET_OBJECT,&
+ output_file = get_object_name(sources(i)))
+
+ if (with_lib .and. sources(i)%unit_scope == FPM_SCOPE_LIB) then
+ ! Archive depends on object
+ call add_dependency(targets(1)%ptr, targets(size(targets))%ptr)
+ end if
+
+ case (FPM_UNIT_PROGRAM)
+
+ call add_target(targets,type = FPM_TARGET_OBJECT,&
+ output_file = get_object_name(sources(i)), &
+ source = sources(i) &
+ )
+
+ if (sources(i)%unit_scope == FPM_SCOPE_APP) then
+
+ exe_dir = 'app'
+
+ else if (sources(i)%unit_scope == FPM_SCOPE_EXAMPLE) then
+
+ exe_dir = 'example'
+
+ else
+
+ exe_dir = 'test'
+
+ end if
+
+ call add_target(targets,type = FPM_TARGET_EXECUTABLE,&
+ link_libraries = sources(i)%link_libraries, &
+ output_file = join_path(model%output_directory,exe_dir, &
+ sources(i)%exe_name//xsuffix))
+
+ ! Executable depends on object
+ call add_dependency(targets(size(targets))%ptr, targets(size(targets)-1)%ptr)
+
+ if (with_lib) then
+ ! Executable depends on library
+ call add_dependency(targets(size(targets))%ptr, targets(1)%ptr)
+ end if
+
+ end select
+
+ end do
+
+ end associate
+
+ end do
+
+ contains
+
+ function get_object_name(source) result(object_file)
+ ! Generate object target path from source name and model params
+ !
+ !
+ type(srcfile_t), intent(in) :: source
+ character(:), allocatable :: object_file
+
+ integer :: i
+ character(1), parameter :: filesep = '/'
+ character(:), allocatable :: dir
+
+ object_file = canon_path(source%file_name)
+
+ ! Convert any remaining directory separators to underscores
+ i = index(object_file,filesep)
+ do while(i > 0)
+ object_file(i:i) = '_'
+ i = index(object_file,filesep)
+ end do
+
+ object_file = join_path(model%output_directory,model%package_name,object_file)//'.o'
+
+ end function get_object_name
+
+end subroutine build_target_list
+
+
+!> Allocate a new target and append to target list
+subroutine add_target(targets,type,output_file,source,link_libraries)
+ type(build_target_ptr), allocatable, intent(inout) :: targets(:)
+ integer, intent(in) :: type
+ character(*), intent(in) :: output_file
+ type(srcfile_t), intent(in), optional :: source
+ type(string_t), intent(in), optional :: link_libraries(:)
+
+ integer :: i
+ type(build_target_ptr), allocatable :: temp(:)
+ type(build_target_t), pointer :: new_target
+
+ if (.not.allocated(targets)) allocate(targets(0))
+
+ ! Check for duplicate outputs
+ do i=1,size(targets)
+
+ if (targets(i)%ptr%output_file == output_file) then
+
+ write(*,*) 'Error while building target list: duplicate output object "',&
+ output_file,'"'
+ if (present(source)) write(*,*) ' Source file: "',source%file_name,'"'
+ stop 1
+
+ end if
+
+ end do
+
+ allocate(new_target)
+ new_target%target_type = type
+ new_target%output_file = output_file
+ if (present(source)) new_target%source = source
+ if (present(link_libraries)) new_target%link_libraries = link_libraries
+ allocate(new_target%dependencies(0))
+
+ targets = [targets, build_target_ptr(new_target)]
+
+end subroutine add_target
+
+
+!> Add pointer to dependeny in target%dependencies
+subroutine add_dependency(target, dependency)
+ type(build_target_t), intent(inout) :: target
+ type(build_target_t) , intent(in), target :: dependency
+
+ target%dependencies = [target%dependencies, build_target_ptr(dependency)]
+
+end subroutine add_dependency
+
+
+!> Add dependencies to source-based targets (`FPM_TARGET_OBJECT`)
+!> based on any modules used by the corresponding source file.
+!>
+!>### Source file scoping
+!>
+!> Source files are assigned a scope of either `FPM_SCOPE_LIB`,
+!> `FPM_SCOPE_APP` or `FPM_SCOPE_TEST`. The scope controls which
+!> modules may be used by the source file:
+!>
+!> - Library sources (`FPM_SCOPE_LIB`) may only use modules
+!> also with library scope. This includes library modules
+!> from dependencies.
+!>
+!> - Executable sources (`FPM_SCOPE_APP`,`FPM_SCOPE_TEST`) may use
+!> library modules (including dependencies) as well as any modules
+!> corresponding to source files in the same directory or a
+!> subdirectory of the executable source file.
+!>
+!> @warning If a module used by a source file cannot be resolved to
+!> a source file in the package of the correct scope, then a __fatal error__
+!> is returned by the procedure and model construction fails.
+!>
+subroutine resolve_module_dependencies(targets,error)
+ type(build_target_ptr), intent(inout), target :: targets(:)
+ type(error_t), allocatable, intent(out) :: error
+
+ type(build_target_ptr) :: dep
+
+ integer :: i, j
+
+ do i=1,size(targets)
+
+ if (.not.allocated(targets(i)%ptr%source)) cycle
+
+ do j=1,size(targets(i)%ptr%source%modules_used)
+
+ if (targets(i)%ptr%source%modules_used(j)%s .in. targets(i)%ptr%source%modules_provided) then
+ ! Dependency satisfied in same file, skip
+ cycle
+ end if
+
+ if (any(targets(i)%ptr%source%unit_scope == &
+ [FPM_SCOPE_APP, FPM_SCOPE_EXAMPLE, FPM_SCOPE_TEST])) then
+ dep%ptr => &
+ find_module_dependency(targets,targets(i)%ptr%source%modules_used(j)%s, &
+ include_dir = dirname(targets(i)%ptr%source%file_name))
+ else
+ dep%ptr => &
+ find_module_dependency(targets,targets(i)%ptr%source%modules_used(j)%s)
+ end if
+
+ if (.not.associated(dep%ptr)) then
+ call fatal_error(error, &
+ 'Unable to find source for module dependency: "' // &
+ targets(i)%ptr%source%modules_used(j)%s // &
+ '" used by "'//targets(i)%ptr%source%file_name//'"')
+ return
+ end if
+
+ call add_dependency(targets(i)%ptr, dep%ptr)
+
+ end do
+
+ end do
+
+end subroutine resolve_module_dependencies
+
+function find_module_dependency(targets,module_name,include_dir) result(target_ptr)
+ ! Find a module dependency in the library or a dependency library
+ !
+ ! 'include_dir' specifies an allowable non-library search directory
+ ! (Used for executable dependencies)
+ !
+ type(build_target_ptr), intent(in), target :: targets(:)
+ character(*), intent(in) :: module_name
+ character(*), intent(in), optional :: include_dir
+ type(build_target_t), pointer :: target_ptr
+
+ integer :: k, l
+
+ target_ptr => NULL()
+
+ do k=1,size(targets)
+
+ if (.not.allocated(targets(k)%ptr%source)) cycle
+
+ do l=1,size(targets(k)%ptr%source%modules_provided)
+
+ if (module_name == targets(k)%ptr%source%modules_provided(l)%s) then
+ select case(targets(k)%ptr%source%unit_scope)
+ case (FPM_SCOPE_LIB, FPM_SCOPE_DEP)
+ target_ptr => targets(k)%ptr
+ exit
+ case default
+ if (present(include_dir)) then
+ if (index(dirname(targets(k)%ptr%source%file_name), include_dir) == 1) then ! source file is within the include_dir or a subdirectory
+ target_ptr => targets(k)%ptr
+ exit
+ end if
+ end if
+ end select
+ end if
+
+ end do
+
+ end do
+
+end function find_module_dependency
+
+
+!> Construct the linker flags string for each target
+!> `target%link_flags` includes non-library objects and library flags
+!>
+subroutine resolve_target_linking(targets, model)
+ type(build_target_ptr), intent(inout), target :: targets(:)
+ type(fpm_model_t), intent(in) :: model
+
+ integer :: i
+ character(:), allocatable :: global_link_flags
+ character(:), allocatable :: global_compile_flags
+
+ if (size(targets) == 0) return
+
+ if (targets(1)%ptr%target_type == FPM_TARGET_ARCHIVE) then
+ global_link_flags = targets(1)%ptr%output_file
+ else
+ allocate(character(0) :: global_link_flags)
+ end if
+
+ global_compile_flags = model%fortran_compile_flags
+
+ if (allocated(model%link_libraries)) then
+ if (size(model%link_libraries) > 0) then
+ global_link_flags = global_link_flags // " -l" // string_cat(model%link_libraries," -l")
+ end if
+ end if
+
+ if (allocated(model%include_dirs)) then
+ if (size(model%include_dirs) > 0) then
+ global_compile_flags = global_compile_flags // &
+ & " -I" // string_cat(model%include_dirs," -I")
+ end if
+ end if
+
+ do i=1,size(targets)
+
+ associate(target => targets(i)%ptr)
+
+ target%compile_flags = global_compile_flags
+
+ allocate(target%link_objects(0))
+
+ if (target%target_type == FPM_TARGET_ARCHIVE) then
+
+ call get_link_objects(target%link_objects,target,is_exe=.false.)
+
+ allocate(character(0) :: target%link_flags)
+
+ else if (target%target_type == FPM_TARGET_EXECUTABLE) then
+
+ call get_link_objects(target%link_objects,target,is_exe=.true.)
+
+ target%link_flags = string_cat(target%link_objects," ")
+
+ if (allocated(target%link_libraries)) then
+ if (size(target%link_libraries) > 0) then
+ target%link_flags = target%link_flags // " -l" // string_cat(target%link_libraries," -l")
+ end if
+ end if
+
+ target%link_flags = target%link_flags//" "//global_link_flags
+
+ end if
+
+ end associate
+
+ end do
+
+contains
+
+ !> Wrapper to build link object list
+ !>
+ !> For libraries: just list dependency objects of lib target
+ !>
+ !> For executables: need to recursively discover non-library
+ !> dependency objects. (i.e. modules in same dir as program)
+ !>
+ recursive subroutine get_link_objects(link_objects,target,is_exe)
+ type(string_t), intent(inout), allocatable :: link_objects(:)
+ type(build_target_t), intent(in) :: target
+ logical, intent(in) :: is_exe
+
+ integer :: i
+ type(string_t) :: temp_str
+
+ if (.not.allocated(target%dependencies)) return
+
+ do i=1,size(target%dependencies)
+
+ associate(dep => target%dependencies(i)%ptr)
+
+ if (.not.allocated(dep%source)) cycle
+
+ ! Skip library dependencies for executable targets
+ ! since the library archive will always be linked
+ if (is_exe.and.(dep%source%unit_scope == FPM_SCOPE_LIB)) cycle
+
+ ! Skip if dependency object already listed
+ if (dep%output_file .in. link_objects) cycle
+
+ ! Add dependency object file to link object list
+ temp_str%s = dep%output_file
+ link_objects = [link_objects, temp_str]
+
+ ! For executable objects, also need to include non-library
+ ! dependencies from dependencies (recurse)
+ if (is_exe) call get_link_objects(link_objects,dep,is_exe=.true.)
+
+ end associate
+
+ end do
+
+ end subroutine get_link_objects
+
+end subroutine resolve_target_linking
+
+
+end module fpm_targets