aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhilipp <pengel@hs-nb.de>2020-09-12 17:12:37 +0200
committerPhilipp <pengel@hs-nb.de>2020-09-12 17:12:37 +0200
commitc7c6ef042b7cf0bbad132dd5824a8446214caec4 (patch)
tree461b75b503fb838b26f05331b6cc976f3017d3ca
parent61b84f8da66fa19be902baf8e6cbe711856f687f (diff)
parente02171d28bb783bb419b44ef453ca56286b389a6 (diff)
downloadfpm-c7c6ef042b7cf0bbad132dd5824a8446214caec4.tar.gz
fpm-c7c6ef042b7cf0bbad132dd5824a8446214caec4.zip
Added new OS type checks.
-rw-r--r--CONTRIBUTING.md126
-rw-r--r--README.md3
-rwxr-xr-xci/run_tests.bat24
-rwxr-xr-xci/run_tests.sh11
-rw-r--r--fpm/app/main.f906
-rw-r--r--fpm/fpm.toml10
-rw-r--r--fpm/src/command_line.f9078
-rw-r--r--fpm/src/fpm.f90170
-rw-r--r--fpm/src/fpm/error.f9058
-rw-r--r--fpm/src/fpm/git.f90170
-rw-r--r--fpm/src/fpm/manifest.f9079
-rw-r--r--fpm/src/fpm/manifest/dependency.f90246
-rw-r--r--fpm/src/fpm/manifest/executable.f90177
-rw-r--r--fpm/src/fpm/manifest/library.f90126
-rw-r--r--fpm/src/fpm/manifest/package.f90275
-rw-r--r--fpm/src/fpm/manifest/test.f90170
-rw-r--r--fpm/src/fpm/toml.f9066
-rw-r--r--fpm/src/fpm_backend.f90123
-rw-r--r--fpm/src/fpm_command_line.f90102
-rw-r--r--fpm/src/fpm_environment.f90 (renamed from fpm/src/environment.f90)21
-rw-r--r--fpm/src/fpm_filesystem.f90230
-rw-r--r--fpm/src/fpm_model.f9065
-rw-r--r--fpm/src/fpm_sources.f90468
-rw-r--r--fpm/src/fpm_strings.f90217
-rw-r--r--fpm/test/main.f9027
-rw-r--r--fpm/test/test_manifest.f90318
-rw-r--r--fpm/test/test_toml.f90107
-rw-r--r--fpm/test/testsuite.f90122
28 files changed, 3407 insertions, 188 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..1332278
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,126 @@
+# Contributing to the Fortran Package Manager
+
+Thank you for considering contributing to the Fortran Package Manager (fpm).
+Please review and follow these guidelines to make the contribution process
+simple and effective for all involved.
+It will help communicate that you respect the time of the community
+developers.
+In return, the community will help address your problem, evaluate changes, and
+guide you through your pull requests.
+
+By contributing to fpm, you certify that you own or are allowed to share the
+content of your contribution under the
+[fpm license](https://github.com/fortran-lang/fpm/blob/master/LICENSE).
+
+* [Style](#style)
+* [Reporting a bug](#reporting-a-bug)
+* [Suggesting a feature](#suggesting-a-feature)
+* [Workflow](#workflow)
+* [General guidelines](#general-guidelines)
+* [For new contributors](#for-new-contributors)
+
+## Style
+
+Please follow the
+[Fortran stdlib style guide](https://github.com/fortran-lang/stdlib/blob/master/STYLE_GUIDE.md)
+for any Fortran code that you contribute.
+This allows us to focus on substance rather than style.
+
+## Reporting a bug
+
+A bug is a _demonstrable problem_ caused by the code in this repository.
+Good bug reports are extremely valuable to us—thank you!
+
+Before opening a bug report:
+
+1. Check if the issue has already been reported.
+ ([issues](https://github.com/fortran-lang/fpm/issues))
+2. Check if it is still an issue or it has been fixed?
+ Try to reproduce it with the latest version from the master branch.
+3. Isolate the problem and create a minimal test case.
+
+A good bug report should include all information needed to reproduce the bug.
+Please be as detailed as possible:
+
+1. Which version of fpm are you using? Please be specific.
+2. What are the steps to reproduce the issue?
+3. What is the expected outcome?
+4. What happens instead?
+
+This information will help the community diagnose the issue quickly and with
+minimal back-and-forth.
+
+## Suggesting a feature
+
+Before suggesting a new feature, take a moment to find out if it fits
+the scope of the project, or if it has already been discussed.
+It is up to you to provide a strong argument to convince the community of the
+benefits of this feature.
+Please provide as much detail and context as possible.
+If applicable, include a mocked-up snippet of what the output or behavior would
+look like with this feature implemented.
+"Crazy", out-of-the-box ideas are especially welcome.
+It's quite possible that we are not considering an unusually creative solution.
+
+## Workflow
+
+fpm is a community project.
+There is no one single person making final decisions.
+This is the workflow that we follow:
+
+1. Open a [new issue](https://github.com/fortran-lang/fpm/issues/new) to
+ describe a bug or propose a new feature.
+ Refer to the earlier sections on how to write a good bug report or feature
+ request.
+2. Discuss with the community and reach majority consensus about what should be
+ done about the bug or feature request.
+ We define "majority" loosely as 80%.
+ This means that at least 4 of 5 people engaged in the discussion should be
+ able to agree on the next step.
+ This allows us to have the community mostly agree while not getting stuck if
+ one person disagrees.
+ At this stage, the scope of the fix/feature, its behavior, and API if
+ applicable should be defined.
+ Only when you have community concensus on these items you should proceed
+ to writing code and opening a PR.
+ __When actively working on code towards a PR, please assign yourself to the issue on github.__
+ This is good collaborative practice to avoid duplicated effort and also inform others what you
+ are currently working on.
+3. Open a new Pull Request (PR) with your contribution.
+ The body of the PR should at least include a bullet-point summary of the
+ changes, and a detailed description is encouraged.
+ If the PR completely addresses the issue you opened in step 1, include in
+ the PR description the following line: `Fixes #<issue-number>`.
+4. Request reviewers to your PR.
+ For small bug fixes or documentation improvements, 1 to 2 reviewers is
+ sufficient.
+ For implementation of bigger features, request 3 to 4 or more reviewers.
+ Ideally, request reviewers that participated in step 2.
+5. If your PR implements a feature that adds or changes the behavior of fpm,
+ your PR must also include appropriate changes to the documentation.
+
+This workflow can evolve and change over time as we learn how best to work
+together.
+If you have an idea on how to improve the workflow itself, please open an issue
+and we'll discuss it.
+
+## General guidelines
+
+* A PR should implement _only one_ feature or bug fix.
+* Do not commit changes to files that are irrelevant to your feature or bug fix.
+* Smaller PRs are better than large PRs, and will lead to a shorter review and
+ merge cycle
+* Add tests for your feature or bug fix to be sure that it stays functional and useful
+* Be open to constructive criticism and requests for improving your code.
+* Again, please follow the
+ [Fortran stdlib style guide](https://github.com/fortran-lang/stdlib/blob/master/STYLE_GUIDE.md).
+
+## For New Contributors
+
+If you have never created a pull request before, welcome :tada:.
+You can learn how from
+[this great tutorial](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github).
+
+Don't know where to start?
+You can start by looking through the list of
+[open issues](https://github.com/fortran-lang/fpm/issues).
diff --git a/README.md b/README.md
index 54244fa..cd73807 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,9 @@ Please follow the [issues](https://github.com/fortran-lang/fpm/issues)
to contribute and/or stay up to date with the development.
As the prototype matures and we enter production, we will do our best to stay backwards compatible.
+To report a bug report or suggest a feature, please read our
+[contributor guidelines](CONTRIBUTING.md).
+
## Getting started
### Install Haskell
diff --git a/ci/run_tests.bat b/ci/run_tests.bat
index 99d0296..9435e0d 100755
--- a/ci/run_tests.bat
+++ b/ci/run_tests.bat
@@ -9,6 +9,9 @@ if errorlevel 1 exit 1
fpm run
if errorlevel 1 exit 1
+fpm test
+if errorlevel 1 exit 1
+
build\gfortran_debug\app\fpm
if errorlevel 1 exit 1
@@ -18,5 +21,24 @@ if errorlevel 1 exit 1
..\..\..\fpm\build\gfortran_debug\app\fpm build
if errorlevel 1 exit 1
-.\hello_world
+.\build\gfortran_debug\app\hello_world
+if errorlevel 1 exit 1
+
+
+cd ..\hello_complex
+if errorlevel 1 exit 1
+
+..\..\..\fpm\build\gfortran_debug\app\fpm build
if errorlevel 1 exit 1
+
+.\build\gfortran_debug\app\say_Hello
+if errorlevel 1 exit 1
+
+.\build\gfortran_debug\app\say_goodbye
+if errorlevel 1 exit 1
+
+.\build\gfortran_debug\test\greet_test
+if errorlevel 1 exit 1
+
+.\build\gfortran_debug\test\farewell_test
+if errorlevel 1 exit 1 \ No newline at end of file
diff --git a/ci/run_tests.sh b/ci/run_tests.sh
index 59724d5..3033c2a 100755
--- a/ci/run_tests.sh
+++ b/ci/run_tests.sh
@@ -5,7 +5,16 @@ set -ex
cd fpm
fpm build
fpm run
+fpm test
build/gfortran_debug/app/fpm
+
cd ../test/example_packages/hello_world
../../../fpm/build/gfortran_debug/app/fpm build
-./hello_world
+./build/gfortran_debug/app/hello_world
+
+cd ../hello_complex
+../../../fpm/build/gfortran_debug/app/fpm build
+./build/gfortran_debug/app/say_Hello
+./build/gfortran_debug/app/say_goodbye
+./build/gfortran_debug/test/greet_test
+./build/gfortran_debug/test/farewell_test \ No newline at end of file
diff --git a/fpm/app/main.f90 b/fpm/app/main.f90
index 30abf5b..c7f9786 100644
--- a/fpm/app/main.f90
+++ b/fpm/app/main.f90
@@ -1,5 +1,5 @@
program main
-use command_line, only: &
+use fpm_command_line, only: &
fpm_cmd_settings, &
fpm_new_settings, &
fpm_build_settings, &
@@ -15,11 +15,11 @@ class(fpm_cmd_settings), allocatable :: cmd_settings
call get_command_line_settings(cmd_settings)
-select type(cmd_settings)
+select type(settings=>cmd_settings)
type is (fpm_new_settings)
call cmd_new()
type is (fpm_build_settings)
- call cmd_build()
+ call cmd_build(settings)
type is (fpm_run_settings)
call cmd_run()
type is (fpm_test_settings)
diff --git a/fpm/fpm.toml b/fpm/fpm.toml
index c07eeba..b39d881 100644
--- a/fpm/fpm.toml
+++ b/fpm/fpm.toml
@@ -4,3 +4,13 @@ license = "MIT"
author = "fpm maintainers"
maintainer = ""
copyright = "2020 fpm contributors"
+
+[dependencies]
+[dependencies.toml-f]
+git = "https://github.com/toml-f/toml-f"
+rev = "290ba87671ab593e7bd51599e1d80ea736b3cd36"
+
+[[test]]
+name = "fpm-test"
+source-dir = "test"
+main = "main.f90"
diff --git a/fpm/src/command_line.f90 b/fpm/src/command_line.f90
deleted file mode 100644
index cd78904..0000000
--- a/fpm/src/command_line.f90
+++ /dev/null
@@ -1,78 +0,0 @@
-module command_line
- use environment, only: get_os_type, OS_LINUX, OS_MACOS, OS_WINDOWS
-
- implicit none
- private
-
- type, public, abstract :: fpm_cmd_settings
- end type
-
- type, public, extends(fpm_cmd_settings) :: fpm_new_settings
- end type
-
- type, public, extends(fpm_cmd_settings) :: fpm_build_settings
- end type
-
- type, public, extends(fpm_cmd_settings) :: fpm_run_settings
- end type
-
- type, public, extends(fpm_cmd_settings) :: fpm_test_settings
- end type
-
- type, public, extends(fpm_cmd_settings) :: fpm_install_settings
- end type
-
- public :: get_command_line_settings
-contains
- subroutine get_command_line_settings(cmd_settings)
- class(fpm_cmd_settings), allocatable, intent(out) :: cmd_settings
-
- character(len=100) :: cmdarg
-
- if (command_argument_count() == 0) then
- call print_help()
- else if (command_argument_count() == 1) then
- call get_command_argument(1, cmdarg)
- select case(trim(cmdarg))
- case("new")
- allocate(fpm_new_settings :: cmd_settings)
- case("build")
- allocate(fpm_build_settings :: cmd_settings)
- case("run")
- allocate(fpm_run_settings :: cmd_settings)
- case("test")
- allocate(fpm_test_settings :: cmd_settings)
- case("install")
- allocate(fpm_install_settings :: cmd_settings)
- case default
- print *, "fpm error: No such command " // trim(cmdarg)
- error stop 1
- end select
- else
- print *, "Too many arguments"
- error stop 1
- end if
- end subroutine
-
- subroutine print_help()
- print *, "fpm - A Fortran package manager and build system"
- select case (get_os_type())
- case (OS_LINUX)
- print *, "OS Type: Linux"
- case (OS_MACOS)
- print *, "OS Type: macOS"
- case (OS_WINDOWS)
- print *, "OS Type: Windows"
- end select
- print *
- print *, "Usage:"
- print *, " fpm [COMMAND]"
- print *
- print *, "Valid fpm commands are:"
- print *, " build Compile the current package"
- print *, " install Install a Fortran binary or library (not implemented)"
- print *, " new Create a new Fortran package (not implemented)"
- print *, " run Run a binary of the local package (not implemented)"
- print *, " test Run the tests (not implemented)"
- end subroutine
-end module command_line
diff --git a/fpm/src/fpm.f90 b/fpm/src/fpm.f90
index ed80313..29d663c 100644
--- a/fpm/src/fpm.f90
+++ b/fpm/src/fpm.f90
@@ -1,118 +1,88 @@
module fpm
-use environment, only: get_os_type, OS_LINUX, OS_MACOS, OS_WINDOWS
+
+use fpm_strings, only: string_t, str_ends_with
+use fpm_backend, only: build_package
+use fpm_command_line, only: fpm_build_settings
+use fpm_environment, only: run, get_os_type, OS_LINUX, OS_MACOS, OS_WINDOWS
+use fpm_filesystem, only: join_path, number_of_rows, list_files, exists
+use fpm_model, only: srcfile_ptr, srcfile_t, fpm_model_t
+use fpm_sources, only: add_executable_sources, add_sources_from_dir, &
+ resolve_module_dependencies
+use fpm_manifest, only : get_package_data, default_executable, &
+ default_library, package_t
+use fpm_error, only : error_t
implicit none
private
public :: cmd_build, cmd_install, cmd_new, cmd_run, cmd_test
-type string_t
- character(len=:), allocatable :: s
-end type
contains
-integer function number_of_rows(s) result(nrows)
-! determine number or rows
-integer,intent(in)::s
-integer :: ios
-character(len=100) :: r
-rewind(s)
-nrows = 0
-do
- read(s, *, iostat=ios) r
- if (ios /= 0) exit
- nrows = nrows + 1
-end do
-rewind(s)
-end function
-
-
-subroutine list_files(dir, files)
-character(len=*), intent(in) :: dir
-type(string_t), allocatable, intent(out) :: files(:)
-character(len=100) :: filename
-integer :: stat, u, i
-! Using `inquire` / exists on directories works with gfortran, but not ifort
-if (.not. exists(dir)) then
- allocate(files(0))
- return
-end if
-select case (get_os_type())
- case (OS_LINUX)
- call execute_command_line("ls " // dir // " > fpm_ls.out", exitstat=stat)
- case (OS_MACOS)
- call execute_command_line("ls " // dir // " > fpm_ls.out", exitstat=stat)
- case (OS_WINDOWS)
- call execute_command_line("dir /b " // dir // " > fpm_ls.out", exitstat=stat)
-end select
-if (stat /= 0) then
- print *, "execute_command_line() failed"
- error stop
+subroutine build_model(model, settings, package)
+ ! 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_t), intent(in) :: package
+
+ model%package_name = package%name
+
+ ! #TODO: Choose flags and output directory based on cli settings & manifest inputs
+ model%fortran_compiler = 'gfortran'
+ model%output_directory = 'build/gfortran_debug'
+ model%fortran_compile_flags = ' -Wall -Wextra -Wimplicit-interface -fPIC -fmax-errors=1 -g '// &
+ '-fbounds-check -fcheck-array-temporaries -fbacktrace '// &
+ '-J'//join_path(model%output_directory,model%package_name)
+ model%link_flags = ''
+
+ ! Add sources from executable directories
+ if (allocated(package%executable)) then
+ call add_executable_sources(model%sources, package%executable,is_test=.false.)
+ end if
+ if (allocated(package%test)) then
+ call add_executable_sources(model%sources, package%test,is_test=.true.)
+ end if
+
+ if (allocated(package%library)) then
+ call add_sources_from_dir(model%sources,package%library%source_dir)
+ end if
+
+ call resolve_module_dependencies(model%sources)
+
+end subroutine build_model
+
+subroutine cmd_build(settings)
+type(fpm_build_settings), intent(in) :: settings
+type(package_t) :: package
+type(fpm_model_t) :: model
+type(error_t), allocatable :: error
+call get_package_data(package, "fpm.toml", error)
+if (allocated(error)) then
+ print '(a)', error%message
+ error stop 1
end if
-open(newunit=u, file="fpm_ls.out", status="old")
-allocate(files(number_of_rows(u)))
-do i = 1, size(files)
- read(u, *) filename
- files(i)%s = trim(filename)
-end do
-close(u)
-end subroutine
-subroutine run(cmd)
-character(len=*), intent(in) :: cmd
-integer :: stat
-print *, "+ ", cmd
-call execute_command_line(cmd, exitstat=stat)
-if (stat /= 0) then
- print *, "Command failed"
- error stop
+! Populate library in case we find the default src directory
+if (.not.allocated(package%library) .and. exists("src")) then
+ allocate(package%library)
+ call default_library(package%library)
end if
-end subroutine
-logical function exists(filename) result(r)
-character(len=*), intent(in) :: filename
-inquire(file=filename, exist=r)
-end function
-
-logical function str_ends_with(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)
+! Populate executable in case we find the default app directory
+if (.not.allocated(package%executable) .and. exists("app")) then
+ allocate(package%executable(1))
+ call default_executable(package%executable(1), package%name)
end if
-end function
-
-subroutine package_name(name)
-character(:), allocatable, intent(out) :: name
-! Currrently a heuristic. We should update this to read the name from fpm.toml
-if (exists("src/fpm.f90")) then
- name = "fpm"
-else
- name = "hello_world"
+
+if (.not.(allocated(package%library) .or. allocated(package%executable))) then
+ print '(a)', "Neither library nor executable found, there is nothing to do"
+ error stop 1
end if
-end subroutine
-subroutine cmd_build()
-type(string_t), allocatable :: files(:)
-character(:), allocatable :: basename, pkg_name, linking
-integer :: i, n
-print *, "# Building project"
-call list_files("src", files)
-linking = ""
-do i = 1, size(files)
- if (str_ends_with(files(i)%s, ".f90")) then
- n = len(files(i)%s)
- basename = files(i)%s(1:n-4)
- call run("gfortran -c src/" // basename // ".f90 -o " // basename // ".o")
- linking = linking // " " // basename // ".o"
- end if
-end do
-call run("gfortran -c app/main.f90 -o main.o")
-call package_name(pkg_name)
-call run("gfortran main.o " // linking // " -o " // pkg_name)
+call build_model(model, settings, package)
+
+call build_package(model)
+
end subroutine
subroutine cmd_install()
diff --git a/fpm/src/fpm/error.f90 b/fpm/src/fpm/error.f90
new file mode 100644
index 0000000..aebd7e4
--- /dev/null
+++ b/fpm/src/fpm/error.f90
@@ -0,0 +1,58 @@
+!> Implementation of basic error handling.
+module fpm_error
+ implicit none
+ private
+
+ public :: error_t
+ public :: fatal_error, syntax_error, file_not_found_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
+
+
+end module fpm_error
diff --git a/fpm/src/fpm/git.f90 b/fpm/src/fpm/git.f90
new file mode 100644
index 0000000..28ae867
--- /dev/null
+++ b/fpm/src/fpm/git.f90
@@ -0,0 +1,170 @@
+!> Implementation for interacting with git repositories.
+module fpm_git
+ implicit none
+
+ public :: git_target_t
+ public :: git_target_default, git_target_branch, git_target_tag, &
+ & git_target_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
+ private
+
+ !> Kind of the git target
+ integer :: 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
+
+ !> 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
+
+
+ !> 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/fpm/src/fpm/manifest.f90 b/fpm/src/fpm/manifest.f90
new file mode 100644
index 0000000..af4e0fa
--- /dev/null
+++ b/fpm/src/fpm/manifest.f90
@@ -0,0 +1,79 @@
+!> 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_executable, only : executable_t
+ use fpm_manifest_library, only : library_t
+ use fpm_manifest_package, only : package_t, new_package
+ use fpm_error, only : error_t, fatal_error, file_not_found_error
+ use fpm_toml, only : toml_table, read_package_file
+ implicit none
+ private
+
+ public :: get_package_data, default_executable, default_library
+ public :: package_t
+
+
+contains
+
+
+ !> Populate library in case we find the default src directory
+ subroutine default_library(self)
+
+ !> Instance of the library meta data
+ type(library_t), intent(out) :: self
+
+ self%source_dir = "src"
+
+ 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_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
+
+
+ !> Obtain package meta data from a configuation file
+ subroutine get_package_data(package, file, error)
+
+ !> Parsed package meta data
+ type(package_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
+
+ type(toml_table), allocatable :: table
+
+ 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)
+
+ end subroutine get_package_data
+
+
+end module fpm_manifest
diff --git a/fpm/src/fpm/manifest/dependency.f90 b/fpm/src/fpm/manifest/dependency.f90
new file mode 100644
index 0000000..8a3d879
--- /dev/null
+++ b/fpm/src/fpm/manifest/dependency.f90
@@ -0,0 +1,246 @@
+!> 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_t, new_dependency, new_dependencies
+
+
+ !> Configuration meta data for a dependency
+ type :: dependency_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_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_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, "revision", 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
+ integer :: ikey
+
+ 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.
+
+ 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 (.not.url_present .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_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
+
+ class(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_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/fpm/src/fpm/manifest/executable.f90 b/fpm/src/fpm/manifest/executable.f90
new file mode 100644
index 0000000..f706001
--- /dev/null
+++ b/fpm/src/fpm/manifest/executable.f90
@@ -0,0 +1,177 @@
+!> 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"
+! [executable.dependencies]
+! ```
+module fpm_manifest_executable
+ use fpm_manifest_dependency, only : dependency_t, new_dependencies
+ use fpm_error, only : error_t, syntax_error
+ use fpm_toml, only : toml_table, toml_key, toml_stat, get_value
+ implicit none
+ private
+
+ public :: executable_t, new_executable
+
+
+ !> Configuation meta data for an executable
+ type :: executable_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_t), allocatable :: dependency(:)
+
+ contains
+
+ !> Print information on this instance
+ procedure :: info
+
+ end type executable_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_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
+
+ class(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
+
+ 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 executable entry")
+ exit
+
+ case("name")
+ name_present = .true.
+
+ case("source-dir", "main", "dependencies")
+ continue
+
+ end select
+ end do
+
+ 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_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/fpm/src/fpm/manifest/library.f90 b/fpm/src/fpm/manifest/library.f90
new file mode 100644
index 0000000..40e5e92
--- /dev/null
+++ b/fpm/src/fpm/manifest/library.f90
@@ -0,0 +1,126 @@
+!> Implementation of the meta data for libraries.
+!
+! A library table can currently have the following fields
+!
+! ```toml
+! [library]
+! source-dir = "path"
+! build-script = "file"
+! ```
+module fpm_manifest_library
+ use fpm_error, only : error_t, syntax_error
+ use fpm_toml, only : toml_table, toml_key, toml_stat, get_value
+ implicit none
+ private
+
+ public :: library_t, new_library
+
+
+ !> Configuration meta data for a library
+ type :: library_t
+
+ !> Source path prefix
+ character(len=:), allocatable :: source_dir
+
+ !> Alternative build script to be invoked
+ character(len=:), allocatable :: build_script
+
+ contains
+
+ !> Print information on this instance
+ procedure :: info
+
+ end type library_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_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)
+
+ 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 package file")
+ exit
+
+ case("source-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_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%build_script)) then
+ write(unit, fmt) "- custom build", self%build_script
+ end if
+
+ end subroutine info
+
+
+end module fpm_manifest_library
diff --git a/fpm/src/fpm/manifest/package.f90 b/fpm/src/fpm/manifest/package.f90
new file mode 100644
index 0000000..4c2c14a
--- /dev/null
+++ b/fpm/src/fpm/manifest/package.f90
@@ -0,0 +1,275 @@
+!> 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]
+! [[executable]]
+! [[test]]
+! ```
+module fpm_manifest_package
+ use fpm_manifest_dependency, only : dependency_t, new_dependencies
+ use fpm_manifest_executable, only : executable_t, new_executable
+ use fpm_manifest_library, only : library_t, new_library
+ use fpm_manifest_test, only : test_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
+ implicit none
+ private
+
+ public :: package_t, new_package
+
+
+ !> Package meta data
+ type :: package_t
+
+ !> Name of the package
+ character(len=:), allocatable :: name
+
+ !> Library meta data
+ type(library_t), allocatable :: library
+
+ !> Executable meta data
+ type(executable_t), allocatable :: executable(:)
+
+ !> Dependency meta data
+ type(dependency_t), allocatable :: dependency(:)
+
+ !> Development dependency meta data
+ type(dependency_t), allocatable :: dev_dependency(:)
+
+ !> Test meta data
+ type(test_t), allocatable :: test(:)
+
+ contains
+
+ !> Print information on this instance
+ procedure :: info
+
+ end type package_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_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
+
+ class(toml_table), pointer :: child, node
+ class(toml_array), pointer :: children
+ 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
+
+ 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
+ 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
+ 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", &
+ & "dependencies", "dev-dependencies", "test", "executable", &
+ & "library")
+ 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_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
+
+ 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%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
+
+
+end module fpm_manifest_package
diff --git a/fpm/src/fpm/manifest/test.f90 b/fpm/src/fpm/manifest/test.f90
new file mode 100644
index 0000000..a6c6f64
--- /dev/null
+++ b/fpm/src/fpm/manifest/test.f90
@@ -0,0 +1,170 @@
+!> 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"
+! [test.dependencies]
+! ```
+module fpm_manifest_test
+ use fpm_manifest_dependency, only : dependency_t, new_dependencies
+ use fpm_manifest_executable, only : executable_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_t, new_test
+
+
+ !> Configuation meta data for an test
+ type, extends(executable_t) :: test_t
+
+ contains
+
+ !> Print information on this instance
+ procedure :: info
+
+ end type test_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_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
+
+ class(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
+
+ 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")
+ continue
+
+ end select
+ end do
+
+ 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_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/fpm/src/fpm/toml.f90 b/fpm/src/fpm/toml.f90
new file mode 100644
index 0000000..183278d
--- /dev/null
+++ b/fpm/src/fpm/toml.f90
@@ -0,0 +1,66 @@
+!> 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: https://github.com/toml-f/toml-f
+module fpm_toml
+ use fpm_error, only : error_t, fatal_error, file_not_found_error
+ use tomlf, only : toml_table, toml_array, toml_key, toml_stat, get_value, &
+ & toml_parse, toml_error
+ use tomlf_type, only : new_table, len
+ implicit none
+ private
+
+ public :: read_package_file
+ public :: toml_table, toml_array, toml_key, toml_stat, get_value
+ public :: new_table, len
+
+
+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
+
+
+end module fpm_toml
diff --git a/fpm/src/fpm_backend.f90 b/fpm/src/fpm_backend.f90
new file mode 100644
index 0000000..62fd242
--- /dev/null
+++ b/fpm/src/fpm_backend.f90
@@ -0,0 +1,123 @@
+module fpm_backend
+
+! Implements the native fpm build backend
+
+use fpm_environment, only: run
+use fpm_filesystem, only: basename, join_path, exists, mkdir
+use fpm_model, only: fpm_model_t, srcfile_t, FPM_UNIT_MODULE, &
+ FPM_UNIT_SUBMODULE, FPM_UNIT_SUBPROGRAM, &
+ FPM_UNIT_CSOURCE, FPM_UNIT_PROGRAM
+use fpm_strings, only: split
+
+implicit none
+
+private
+public :: build_package
+
+contains
+
+
+subroutine build_package(model)
+ type(fpm_model_t), intent(inout) :: model
+
+ integer :: i
+ character(:), allocatable :: base, linking, subdir
+
+ if (.not.exists(model%output_directory)) then
+ call mkdir(model%output_directory)
+ end if
+ if (.not.exists(join_path(model%output_directory,model%package_name))) then
+ call mkdir(join_path(model%output_directory,model%package_name))
+ end if
+
+ linking = ""
+ do i=1,size(model%sources)
+
+ if (model%sources(i)%unit_type == FPM_UNIT_MODULE .or. &
+ model%sources(i)%unit_type == FPM_UNIT_SUBMODULE .or. &
+ model%sources(i)%unit_type == FPM_UNIT_SUBPROGRAM .or. &
+ model%sources(i)%unit_type == FPM_UNIT_CSOURCE) then
+
+ call build_source(model,model%sources(i),linking)
+
+ end if
+
+ end do
+
+ if (any([(model%sources(i)%unit_type == FPM_UNIT_PROGRAM,i=1,size(model%sources))])) then
+ if (.not.exists(join_path(model%output_directory,'test'))) then
+ call mkdir(join_path(model%output_directory,'test'))
+ end if
+ if (.not.exists(join_path(model%output_directory,'app'))) then
+ call mkdir(join_path(model%output_directory,'app'))
+ end if
+ end if
+
+ do i=1,size(model%sources)
+
+ if (model%sources(i)%unit_type == FPM_UNIT_PROGRAM) then
+
+ base = basename(model%sources(i)%file_name,suffix=.false.)
+
+ if (model%sources(i)%is_test) then
+ subdir = 'test'
+ else
+ subdir = 'app'
+ end if
+
+ call run("gfortran -c " // model%sources(i)%file_name // ' '//model%fortran_compile_flags &
+ // " -o " // join_path(model%output_directory,subdir,base) // ".o")
+
+ call run("gfortran " // join_path(model%output_directory, subdir, base) // ".o "// &
+ linking //" " //model%link_flags // " -o " // &
+ join_path(model%output_directory,subdir,model%sources(i)%exe_name) )
+
+ end if
+
+ end do
+
+end subroutine build_package
+
+
+
+recursive subroutine build_source(model,source_file,linking)
+ ! Compile Fortran source, called recursively on it dependents
+ !
+ type(fpm_model_t), intent(in) :: model
+ type(srcfile_t), intent(inout) :: source_file
+ character(:), allocatable, intent(inout) :: linking
+
+ integer :: i
+ character(:), allocatable :: object_file
+
+ if (source_file%built) then
+ return
+ end if
+
+ if (source_file%touched) then
+ write(*,*) '(!) Circular dependency found with: ',source_file%file_name
+ stop
+ else
+ source_file%touched = .true.
+ end if
+
+ do i=1,size(source_file%file_dependencies)
+
+ if (associated(source_file%file_dependencies(i)%ptr)) then
+ call build_source(model,source_file%file_dependencies(i)%ptr,linking)
+ end if
+
+ end do
+
+ object_file = join_path(model%output_directory, model%package_name, &
+ basename(source_file%file_name,suffix=.false.)//'.o')
+
+ call run("gfortran -c " // source_file%file_name // model%fortran_compile_flags &
+ // " -o " // object_file)
+ linking = linking // " " // object_file
+
+ source_file%built = .true.
+
+end subroutine build_source
+
+end module fpm_backend
diff --git a/fpm/src/fpm_command_line.f90 b/fpm/src/fpm_command_line.f90
new file mode 100644
index 0000000..fdb4649
--- /dev/null
+++ b/fpm/src/fpm_command_line.f90
@@ -0,0 +1,102 @@
+module fpm_command_line
+ use :: fpm_environment, only: get_os_type, &
+ OS_UNKNOWN, OS_LINUX, OS_MACOS, OS_WINDOWS, &
+ OS_CYGWIN, OS_SOLARIS, OS_FREEBSD
+ implicit none
+
+ private
+ public :: fpm_cmd_settings, &
+ fpm_build_settings, &
+ fpm_install_settings, &
+ fpm_new_settings, &
+ fpm_run_settings, &
+ fpm_test_settings, &
+ get_command_line_settings
+
+ type, abstract :: fpm_cmd_settings
+ end type
+
+ type, extends(fpm_cmd_settings) :: fpm_new_settings
+ end type
+
+ type, extends(fpm_cmd_settings) :: fpm_build_settings
+ end type
+
+ type, extends(fpm_cmd_settings) :: fpm_run_settings
+ end type
+
+ type, extends(fpm_cmd_settings) :: fpm_test_settings
+ end type
+
+ type, extends(fpm_cmd_settings) :: fpm_install_settings
+ end type
+
+contains
+ subroutine get_command_line_settings(cmd_settings)
+ class(fpm_cmd_settings), allocatable, intent(out) :: cmd_settings
+
+ character(len=100) :: cmdarg
+
+ if (command_argument_count() == 0) then
+ call print_help()
+ else if (command_argument_count() == 1) then
+ call get_command_argument(1, cmdarg)
+ select case(trim(cmdarg))
+ case("new")
+ allocate(fpm_new_settings :: cmd_settings)
+ case("build")
+ allocate(fpm_build_settings :: cmd_settings)
+ case("run")
+ allocate(fpm_run_settings :: cmd_settings)
+ case("test")
+ allocate(fpm_test_settings :: cmd_settings)
+ case("install")
+ allocate(fpm_install_settings :: cmd_settings)
+ case default
+ print *, "fpm error: No such command " // trim(cmdarg)
+ error stop 1
+ end select
+ else
+ print *, "Too many arguments"
+ error stop 1
+ end if
+ end subroutine
+
+ subroutine print_help()
+ print *, 'fpm - A Fortran package manager and build system'
+
+ select case (get_os_type())
+ case (OS_UNKNOWN)
+ print *, 'OS Type: Unknown'
+
+ case (OS_LINUX)
+ print *, 'OS Type: Linux'
+
+ case (OS_MACOS)
+ print *, 'OS Type: macOS'
+
+ case (OS_WINDOWS)
+ print *, 'OS Type: Windows'
+
+ case (OS_CYGWIN)
+ print *, 'OS Type: Cygwin'
+
+ case (OS_SOLARIS)
+ print *, 'OS Type: Solaris'
+
+ case (OS_FREEBSD)
+ print *, 'OS Type: FreeBSD'
+ end select
+
+ print *
+ print *, 'Usage:'
+ print *, ' fpm [COMMAND]'
+ print *
+ print *, 'Valid fpm commands are:'
+ print *, ' build Compile the current package'
+ print *, ' install Install a Fortran binary or library (not implemented)'
+ print *, ' new Create a new Fortran package (not implemented)'
+ print *, ' run Run a binary of the local package (not implemented)'
+ print *, ' test Run the tests (not implemented)'
+ end subroutine
+end module fpm_command_line
diff --git a/fpm/src/environment.f90 b/fpm/src/fpm_environment.f90
index ec5230c..553aa8b 100644
--- a/fpm/src/environment.f90
+++ b/fpm/src/fpm_environment.f90
@@ -1,6 +1,8 @@
-module environment
+module fpm_environment
implicit none
private
+ public :: get_os_type
+ public :: run
integer, parameter, public :: OS_UNKNOWN = 0
integer, parameter, public :: OS_LINUX = 1
@@ -9,8 +11,6 @@ module environment
integer, parameter, public :: OS_CYGWIN = 4
integer, parameter, public :: OS_SOLARIS = 5
integer, parameter, public :: OS_FREEBSD = 6
-
- public :: get_os_type
contains
integer function get_os_type() result(r)
!! Determine the OS type
@@ -20,7 +20,7 @@ contains
!!
!! 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 existance of files that can be
+ !! 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.
@@ -103,4 +103,15 @@ contains
return
end if
end function get_os_type
-end module environment
+
+ subroutine run(cmd)
+ character(len=*), intent(in) :: cmd
+ integer :: stat
+ print *, '+ ', cmd
+ call execute_command_line(cmd, exitstat=stat)
+ if (stat /= 0) then
+ print *, 'Command failed'
+ error stop
+ end if
+ end subroutine run
+end module fpm_environment
diff --git a/fpm/src/fpm_filesystem.f90 b/fpm/src/fpm_filesystem.f90
new file mode 100644
index 0000000..090c3d7
--- /dev/null
+++ b/fpm/src/fpm_filesystem.f90
@@ -0,0 +1,230 @@
+module fpm_filesystem
+ 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, string_t, split
+ implicit none
+ private
+ public :: basename, join_path, number_of_rows, read_lines, list_files, &
+ mkdir, exists, get_temp_filename, windows_path
+
+ integer, parameter :: LINE_BUFFER_LEN = 1000
+
+contains
+
+
+function basename(path,suffix) result (base)
+ ! Extract filename from path with/without suffix
+ !
+ 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='\/')
+ base = trim(file_parts(size(file_parts)))
+ else
+ call split(path,file_parts,delimiters='\/.')
+ base = trim(file_parts(size(file_parts)-1))
+ end if
+
+end function basename
+
+
+function join_path(a1,a2,a3,a4,a5) result(path)
+ ! Construct path by joining strings with os file separator
+ !
+ 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
+
+
+integer function number_of_rows(s) result(nrows)
+ ! determine number or rows
+ 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
+
+
+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
+
+subroutine mkdir(dir)
+ character(len=*), 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('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
+
+
+subroutine list_files(dir, files)
+ character(len=*), intent(in) :: dir
+ type(string_t), allocatable, intent(out) :: files(:)
+ character(len=:), allocatable :: temp_file
+ integer :: stat, fh
+
+ ! Using `inquire` / exists on directories works with gfortran, but not ifort
+ if (.not. exists(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 ' // 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')
+end subroutine list_files
+
+
+logical function exists(filename) result(r)
+ character(len=*), intent(in) :: filename
+ inquire(file=filename, exist=r)
+end function
+
+
+function get_temp_filename() result(tempfile)
+ ! 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
+ !
+ 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
+
+
+function windows_path(path) result(winpath)
+ ! Replace file system separators for windows
+ !
+ 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
+
+end module fpm_filesystem
diff --git a/fpm/src/fpm_model.f90 b/fpm/src/fpm_model.f90
new file mode 100644
index 0000000..702ba6f
--- /dev/null
+++ b/fpm/src/fpm_model.f90
@@ -0,0 +1,65 @@
+module fpm_model
+! Definition and validation of the backend model
+use fpm_strings, only: string_t
+implicit none
+
+private
+public :: srcfile_ptr, srcfile_t, fpm_model_t
+
+public :: FPM_UNIT_UNKNOWN, FPM_UNIT_PROGRAM, FPM_UNIT_MODULE, &
+ FPM_UNIT_SUBMODULE, FPM_UNIT_SUBPROGRAM, FPM_UNIT_CSOURCE, &
+ FPM_UNIT_CHEADER
+
+integer, parameter :: FPM_UNIT_UNKNOWN = -1
+integer, parameter :: FPM_UNIT_PROGRAM = 1
+integer, parameter :: FPM_UNIT_MODULE = 2
+integer, parameter :: FPM_UNIT_SUBMODULE = 3
+integer, parameter :: FPM_UNIT_SUBPROGRAM = 4
+integer, parameter :: FPM_UNIT_CSOURCE = 5
+integer, parameter :: FPM_UNIT_CHEADER = 6
+
+type srcfile_ptr
+ ! For constructing arrays of src_file pointers
+ type(srcfile_t), pointer :: ptr => null()
+end type srcfile_ptr
+
+type srcfile_t
+ ! Type for encapsulating a source file
+ ! and it's metadata
+ character(:), allocatable :: file_name
+ ! File path relative to cwd
+ character(:), allocatable :: exe_name
+ ! Name of executable for FPM_UNIT_PROGRAM
+ logical :: is_test = .false.
+ ! Is executable a test?
+ type(string_t), allocatable :: modules_provided(:)
+ ! Modules provided by this source file (lowerstring)
+ integer :: unit_type = FPM_UNIT_UNKNOWN
+ ! Type of program unit
+ type(string_t), allocatable :: modules_used(:)
+ ! Modules USEd by this source file (lowerstring)
+ type(string_t), allocatable :: include_dependencies(:)
+ ! Files INCLUDEd by this source file
+ type(srcfile_ptr), allocatable :: file_dependencies(:)
+ ! Resolved source file dependencies
+
+ logical :: built = .false.
+ logical :: touched = .false.
+end type srcfile_t
+
+type :: fpm_model_t
+ character(:), allocatable :: package_name
+ ! Name of package
+ type(srcfile_t), allocatable :: sources(:)
+ ! Array of sources with module-dependencies resolved
+ character(:), allocatable :: fortran_compiler
+ ! Command line name to invoke fortran compiler
+ character(:), allocatable :: fortran_compile_flags
+ ! Command line flags passed to fortran for compilation
+ character(:), allocatable :: link_flags
+ ! Command line flags pass for linking
+ character(:), allocatable :: output_directory
+ ! Base directory for build
+end type fpm_model_t
+
+end module fpm_model
diff --git a/fpm/src/fpm_sources.f90 b/fpm/src/fpm_sources.f90
new file mode 100644
index 0000000..f2418b5
--- /dev/null
+++ b/fpm/src/fpm_sources.f90
@@ -0,0 +1,468 @@
+module fpm_sources
+use fpm_model, only: srcfile_ptr, srcfile_t, fpm_model_t, &
+ FPM_UNIT_UNKNOWN, FPM_UNIT_PROGRAM, FPM_UNIT_MODULE, &
+ FPM_UNIT_SUBMODULE, FPM_UNIT_SUBPROGRAM, &
+ FPM_UNIT_CSOURCE, FPM_UNIT_CHEADER
+use fpm_filesystem, only: basename, read_lines, list_files
+use fpm_strings, only: lower, split, str_ends_with, string_t, operator(.in.)
+use fpm_manifest_executable, only: executable_t
+implicit none
+
+private
+public :: add_sources_from_dir, add_executable_sources, resolve_module_dependencies
+
+character(15), parameter :: INTRINSIC_MODULE_NAMES(*) = &
+ ['iso_c_binding ', &
+ 'iso_fortran_env', &
+ 'ieee_arithmetic', &
+ 'ieee_exceptions', &
+ 'ieee_features ']
+
+contains
+
+subroutine add_sources_from_dir(sources,directory,with_executables)
+ ! Enumerate sources in a directory
+ !
+ type(srcfile_t), allocatable, intent(inout), target :: sources(:)
+ character(*), intent(in) :: directory
+ logical, intent(in), optional :: with_executables
+
+ integer :: i, j
+ logical, allocatable :: is_source(:), exclude_source(:)
+ type(string_t), allocatable :: file_names(:)
+ type(string_t), allocatable :: src_file_names(:)
+ type(srcfile_t), allocatable :: dir_sources(:)
+
+ ! Scan directory for sources
+ call list_files(directory, file_names)
+ file_names = [(string_t(directory//'/'//file_names(j)%s),j=1,size(file_names))]
+
+ is_source = [(str_ends_with(lower(file_names(i)%s), ".f90") .or. &
+ str_ends_with(lower(file_names(i)%s), ".c") .or. &
+ str_ends_with(lower(file_names(i)%s), ".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)
+
+ if (str_ends_with(lower(src_file_names(i)%s), ".f90")) then
+ dir_sources(i) = parse_f_source(src_file_names(i)%s)
+ end if
+
+ if (str_ends_with(lower(src_file_names(i)%s), ".c") .or. &
+ str_ends_with(lower(src_file_names(i)%s), ".h")) then
+ dir_sources(i) = parse_c_source(src_file_names(i)%s)
+ end if
+
+ ! 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.
+ dir_sources(i)%exe_name = basename(src_file_names(i)%s,suffix=.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
+
+
+subroutine add_executable_sources(sources,executables,is_test)
+ ! Add sources from executable directories specified in manifest
+ ! Only allow executables that are explicitly specified in manifest
+ !
+ type(srcfile_t), allocatable, intent(inout), target :: sources(:)
+ class(executable_t), intent(in) :: executables(:)
+ logical, intent(in) :: is_test
+
+ integer :: i, j
+
+ type(string_t), allocatable :: exe_dirs(:)
+ logical, allocatable :: exclude_source(:)
+ type(srcfile_t), allocatable :: dir_sources(:)
+
+ call get_executable_source_dirs(exe_dirs,executables)
+
+ do i=1,size(exe_dirs)
+ call add_sources_from_dir(dir_sources,exe_dirs(i)%s, &
+ with_executables=.true.)
+ end do
+
+ allocate(exclude_source(size(dir_sources)))
+
+ do i = 1, size(dir_sources)
+
+ ! Only allow executables in 'executables' list
+ exclude_source(i) = (dir_sources(i)%unit_type == FPM_UNIT_PROGRAM)
+
+ do j=1,size(executables)
+ if (basename(dir_sources(i)%file_name,suffix=.true.) == &
+ executables(j)%main) then
+ exclude_source(i) = .false.
+ dir_sources(i)%exe_name = executables(j)%name
+ dir_sources(i)%is_test = is_test
+ exit
+ end if
+ end do
+
+ 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_executable_sources
+
+
+subroutine get_executable_source_dirs(exe_dirs,executables)
+ ! Build a list of unique source directories
+ ! from executables specified in manifest
+ type(string_t), allocatable, intent(inout) :: exe_dirs(:)
+ class(executable_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
+
+
+function parse_f_source(f_filename) result(f_source)
+ ! Rudimentary scan of Fortran source file and
+ ! extract program unit name and use/include dependencies
+ !
+ character(*), intent(in) :: f_filename
+ type(srcfile_t) :: f_source
+
+ integer :: fh, n_use, n_include, n_mod, i, j, ic, pass
+ type(string_t), allocatable :: file_lines(:)
+ character(:), allocatable :: line_parts(:)
+ 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)
+
+ 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
+
+ call split(file_lines(i)%s,line_parts,delimiters=':')
+ temp_string = trim(line_parts(2))
+ call split(temp_string,line_parts,delimiters=' ,')
+ mod_name = trim(lower(line_parts(1)))
+
+ else
+
+ call split(file_lines(i)%s,line_parts,delimiters=' ,')
+ mod_name = trim(lower(line_parts(2)))
+
+ 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
+ if (index(adjustl(lower(file_lines(i)%s)),'include') == 1) then
+
+ n_include = n_include + 1
+
+ if (pass == 2) then
+ call split(file_lines(i)%s,line_parts,delimiters="'"//'"')
+ f_source%include_dependencies(n_include)%s = trim(line_parts(2))
+ end if
+
+ end if
+
+ ! Extract name of module if is module
+ if (index(adjustl(lower(file_lines(i)%s)),'module ') == 1) then
+
+ call split(file_lines(i)%s,line_parts,delimiters=' ')
+
+ mod_name = adjustl(trim(lower(line_parts(2))))
+
+ if (.not.validate_name(mod_name)) then
+ cycle
+ 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
+
+ call split(file_lines(i)%s,line_parts,delimiters=' ()')
+
+ f_source%unit_type = FPM_UNIT_SUBMODULE
+
+ n_use = n_use + 1
+
+ if (pass == 2) then
+
+ if (index(line_parts(2),':') > 0) then
+
+ line_parts(2) = line_parts(2)(index(line_parts(2),':')+1:)
+
+ end if
+
+ f_source%modules_used(n_use)%s = adjustl(trim(lower(line_parts(2))))
+
+ end if
+
+ end if
+
+ ! Detect if is program
+ if (f_source%unit_type == FPM_UNIT_UNKNOWN .and. &
+ index(adjustl(lower(file_lines(i)%s)),'program') == 1) then
+
+ 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 (trim(lower(name)) == 'procedure' .or. &
+ trim(lower(name)) == 'subroutine' .or. &
+ trim(lower(name)) == 'function') 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
+
+
+function parse_c_source(c_filename) result(c_source)
+ ! Rudimentary scan of c source file and
+ ! extract include dependencies
+ !
+ character(*), intent(in) :: c_filename
+ type(srcfile_t) :: c_source
+
+ integer :: fh, n_include, i, pass
+ type(string_t), allocatable :: file_lines(:)
+ character(:), allocatable :: line_parts(:)
+
+ 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)
+
+ 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
+ call split(file_lines(i)%s,line_parts,delimiters='"')
+ c_source%include_dependencies(n_include)%s = trim(line_parts(2))
+ 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
+
+
+subroutine resolve_module_dependencies(sources)
+ ! After enumerating all source files: resolve file dependencies
+ ! by searching on module names
+ !
+ type(srcfile_t), intent(inout), target :: sources(:)
+
+ integer :: n_depend, i, j
+
+ do i=1,size(sources)
+
+ n_depend = size(sources(i)%modules_used)
+
+ allocate(sources(i)%file_dependencies(n_depend))
+
+ do j=1,n_depend
+
+ sources(i)%file_dependencies(j)%ptr => &
+ find_module_dependency(sources,sources(i)%modules_used(j)%s)
+
+ if (.not.associated(sources(i)%file_dependencies(j)%ptr)) then
+ write(*,*) '(!) Unable to find source for module dependency: ',sources(i)%modules_used(j)%s
+ ! stop
+ end if
+
+ end do
+
+ end do
+
+end subroutine resolve_module_dependencies
+
+function find_module_dependency(sources,module_name) result(src_ptr)
+ type(srcfile_t), intent(in), target :: sources(:)
+ character(*), intent(in) :: module_name
+ type(srcfile_t), pointer :: src_ptr
+
+ integer :: k, l
+
+ src_ptr => NULL()
+
+ do k=1,size(sources)
+
+ do l=1,size(sources(k)%modules_provided)
+
+ if (module_name == sources(k)%modules_provided(l)%s) then
+ src_ptr => sources(k)
+ exit
+ end if
+
+ end do
+
+ end do
+
+end function find_module_dependency
+
+end module fpm_sources
diff --git a/fpm/src/fpm_strings.f90 b/fpm/src/fpm_strings.f90
new file mode 100644
index 0000000..a6511c9
--- /dev/null
+++ b/fpm/src/fpm_strings.f90
@@ -0,0 +1,217 @@
+module fpm_strings
+implicit none
+
+private
+public :: f_string, lower, split, str_ends_with, string_t
+public :: string_array_contains, operator(.in.)
+
+type string_t
+ character(len=:), allocatable :: s
+end type
+
+interface operator(.in.)
+ module procedure string_array_contains
+end interface
+
+contains
+
+logical function str_ends_with(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
+
+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
+
+
+elemental pure function lower(str,begin,end) result (string)
+ ! Changes a string to lowercase over specified range
+ ! Author: John S. Urban
+ ! License: Public Domain
+
+ 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
+
+
+logical function string_array_contains(search_string,array)
+ ! Check if array of string_t contains a particular string
+ !
+ 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
+
+
+subroutine split(input_line,array,delimiters,order,nulls)
+ ! parse string on delimiter characters and store tokens into an allocatable array"
+ ! Author: John S. Urban
+ ! License: Public Domain
+
+
+ ! given a line of structure " par1 par2 par3 ... parn " store each par(n) into a separate variable in array.
+ ! o by default adjacent delimiters in the input string do not create an empty string in the output array
+ ! o 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
+
+
+end module fpm_strings
diff --git a/fpm/test/main.f90 b/fpm/test/main.f90
new file mode 100644
index 0000000..19bcdb6
--- /dev/null
+++ b/fpm/test/main.f90
@@ -0,0 +1,27 @@
+!> Driver for unit testing
+program fpm_testing
+ use, intrinsic :: iso_fortran_env, only : error_unit
+ use testsuite, only : run_testsuite
+ use test_toml, only : collect_toml
+ use test_manifest, only : collect_manifest
+ implicit none
+ integer :: stat
+ character(len=*), parameter :: fmt = '("#", *(1x, a))'
+
+ write(error_unit, fmt) "Testing:", "fpm_toml"
+ call run_testsuite(collect_toml, error_unit, stat)
+
+ if (stat > 0) then
+ write(error_unit, '(i0, 1x, a)') stat, "tests failed!"
+ error stop 1
+ end if
+
+ write(error_unit, fmt) "Testing:", "fpm_manifest"
+ call run_testsuite(collect_manifest, error_unit, stat)
+
+ if (stat > 0) then
+ write(error_unit, '(i0, 1x, a)') stat, "tests failed!"
+ error stop 1
+ end if
+
+end program fpm_testing
diff --git a/fpm/test/test_manifest.f90 b/fpm/test/test_manifest.f90
new file mode 100644
index 0000000..223b346
--- /dev/null
+++ b/fpm/test/test_manifest.f90
@@ -0,0 +1,318 @@
+!> Define tests for the `fpm_manifest` modules
+module test_manifest
+ use testsuite, only : new_unittest, unittest_t, error_t, test_failed
+ use fpm_manifest
+ implicit none
+ private
+
+ public :: collect_manifest
+
+
+contains
+
+
+ !> Collect all exported unit tests
+ subroutine collect_manifest(testsuite)
+
+ !> Collection of tests
+ type(unittest_t), allocatable, intent(out) :: testsuite(:)
+
+ testsuite = [ &
+ & new_unittest("valid-manifest", test_valid_manifest), &
+ & new_unittest("invalid-manifest", test_invalid_manifest, should_fail=.true.), &
+ & new_unittest("default-library", test_default_library), &
+ & new_unittest("default-executable", test_default_executable), &
+ & new_unittest("dependency-empty", test_dependency_empty, should_fail=.true.), &
+ & new_unittest("dependencies-empty", test_dependencies_empty), &
+ & new_unittest("executable-empty", test_executable_empty, should_fail=.true.), &
+ & new_unittest("library-empty", test_library_empty), &
+ & new_unittest("package-empty", test_package_empty, should_fail=.true.), &
+ & new_unittest("test-empty", test_test_empty, should_fail=.true.)]
+
+ end subroutine collect_manifest
+
+
+ !> Try to read some unnecessary obscure and convoluted but not invalid package file
+ subroutine test_valid_manifest(error)
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(package_t) :: package
+ character(len=*), parameter :: manifest = 'fpm-valid-manifest.toml'
+ integer :: unit
+
+ open(file=manifest, newunit=unit)
+ write(unit, '(a)') &
+ & 'name = "example"', &
+ & '[dependencies.fpm]', &
+ & 'git = "https://github.com/fortran-lang/fpm"', &
+ & '[[executable]]', &
+ & 'name = "example-#1" # comment', &
+ & 'source-dir = "prog"', &
+ & '[dependencies]', &
+ & 'toml-f.git = "git@github.com:toml-f/toml-f.git"', &
+ & '"toml..f" = { path = ".." }', &
+ & '[["executable"]]', &
+ & 'name = "example-#2"', &
+ & 'source-dir = "prog"', &
+ & '[executable.dependencies]', &
+ & '[''library'']', &
+ & 'source-dir = """', &
+ & 'lib""" # comment'
+ close(unit)
+
+ call get_package_data(package, manifest, error)
+
+ open(file=manifest, newunit=unit)
+ close(unit, status='delete')
+
+ if (allocated(error)) return
+
+ if (package%name /= "example") then
+ call test_failed(error, "Package name is "//package%name//" but should be example")
+ return
+ end if
+
+ if (.not.allocated(package%library)) then
+ call test_failed(error, "library is not present in package data")
+ return
+ end if
+
+ if (.not.allocated(package%executable)) then
+ call test_failed(error, "executable is not present in package data")
+ return
+ end if
+
+ if (size(package%executable) /= 2) then
+ call test_failed(error, "Number of executables in package is not two")
+ return
+ end if
+
+ if (.not.allocated(package%dependency)) then
+ call test_failed(error, "dependency is not present in package data")
+ return
+ end if
+
+ if (size(package%dependency) /= 3) then
+ call test_failed(error, "Number of dependencies in package is not three")
+ return
+ end if
+
+ if (allocated(package%test)) then
+ call test_failed(error, "test is present in package but not in package file")
+ return
+ end if
+
+ end subroutine test_valid_manifest
+
+
+ !> Try to read a valid TOML document which represent an invalid package file
+ subroutine test_invalid_manifest(error)
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(package_t) :: package
+ character(len=*), parameter :: manifest = 'fpm-invalid-manifest.toml'
+ integer :: unit
+
+ open(file=manifest, newunit=unit)
+ write(unit, '(a)') &
+ & '[package]', &
+ & 'name = "example"', &
+ & 'version = "0.1.0"'
+ close(unit)
+
+ call get_package_data(package, manifest, error)
+
+ open(file=manifest, newunit=unit)
+ close(unit, status='delete')
+
+ end subroutine test_invalid_manifest
+
+
+ !> Create a default library
+ subroutine test_default_library(error)
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(package_t) :: package
+
+ allocate(package%library)
+ call default_library(package%library)
+
+ if (.not.allocated(package%library%source_dir)) then
+ call test_failed(error, "Default library source-dir is not set")
+ return
+ end if
+
+ if (package%library%source_dir /= "src") then
+ call test_failed(error, "Default library source-dir is "// &
+ & package%library%source_dir//" but should be src")
+ return
+ end if
+
+ end subroutine test_default_library
+
+
+ !> Create a default executable
+ subroutine test_default_executable(error)
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(package_t) :: package
+ character(len=*), parameter :: name = "default"
+
+ allocate(package%executable(1))
+ call default_executable(package%executable(1), name)
+
+ if (.not.allocated(package%executable(1)%source_dir)) then
+ call test_failed(error, "Default executable source-dir is not set")
+ return
+ end if
+
+ if (package%executable(1)%source_dir /= "app") then
+ call test_failed(error, "Default executable source-dir is "// &
+ & package%executable(1)%source_dir//" but should be app")
+ return
+ end if
+
+ if (package%executable(1)%name /= name) then
+ call test_failed(error, "Default executable name is "// &
+ & package%executable(1)%name//" but should be "//name)
+ return
+ end if
+
+ end subroutine test_default_executable
+
+
+ !> Dependencies cannot be created from empty tables
+ subroutine test_dependency_empty(error)
+ use fpm_manifest_dependency
+ use fpm_toml, only : new_table, toml_table
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_table) :: table
+ type(dependency_t) :: dependency
+
+ call new_table(table)
+ table%key = "example"
+
+ call new_dependency(dependency, table, error)
+
+ end subroutine test_dependency_empty
+
+
+ !> Dependency tables can be empty
+ subroutine test_dependencies_empty(error)
+ use fpm_manifest_dependency
+ use fpm_toml, only : new_table, toml_table
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_table) :: table
+ type(dependency_t), allocatable :: dependencies(:)
+
+ call new_table(table)
+
+ call new_dependencies(dependencies, table, error)
+ if (allocated(error)) return
+
+ if (allocated(dependencies)) then
+ call test_failed(error, "Found dependencies in empty table")
+ end if
+
+ end subroutine test_dependencies_empty
+
+
+ !> Executables cannot be created from empty tables
+ subroutine test_executable_empty(error)
+ use fpm_manifest_executable
+ use fpm_toml, only : new_table, toml_table
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_table) :: table
+ type(executable_t) :: executable
+
+ call new_table(table)
+
+ call new_executable(executable, table, error)
+
+ end subroutine test_executable_empty
+
+
+ !> Libraries can be created from empty tables
+ subroutine test_library_empty(error)
+ use fpm_manifest_library
+ use fpm_toml, only : new_table, toml_table
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_table) :: table
+ type(library_t) :: library
+
+ call new_table(table)
+
+ call new_library(library, table, error)
+ if (allocated(error)) return
+
+ if (.not.allocated(library%source_dir)) then
+ call test_failed(error, "Default library source-dir is not set")
+ return
+ end if
+
+ if (library%source_dir /= "src") then
+ call test_failed(error, "Default library source-dir is "// &
+ & library%source_dir//" but should be src")
+ return
+ end if
+
+ end subroutine test_library_empty
+
+
+ !> Packages cannot be created from empty tables
+ subroutine test_package_empty(error)
+ use fpm_manifest_package
+ use fpm_toml, only : new_table, toml_table
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_table) :: table
+ type(package_t) :: package
+
+ call new_table(table)
+
+ call new_package(package, table, error)
+
+ end subroutine test_package_empty
+
+
+ !> Tests cannot be created from empty tables
+ subroutine test_test_empty(error)
+ use fpm_manifest_test
+ use fpm_toml, only : new_table, toml_table
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_table) :: table
+ type(test_t) :: test
+
+ call new_table(table)
+
+ call new_test(test, table, error)
+
+ end subroutine test_test_empty
+
+
+end module test_manifest
diff --git a/fpm/test/test_toml.f90 b/fpm/test/test_toml.f90
new file mode 100644
index 0000000..ba48307
--- /dev/null
+++ b/fpm/test/test_toml.f90
@@ -0,0 +1,107 @@
+!> Define tests for the `fpm_toml` modules
+module test_toml
+ use testsuite, only : new_unittest, unittest_t, error_t
+ use fpm_toml
+ implicit none
+ private
+
+ public :: collect_toml
+
+
+contains
+
+
+ !> Collect all exported unit tests
+ subroutine collect_toml(testsuite)
+
+ !> Collection of tests
+ type(unittest_t), allocatable, intent(out) :: testsuite(:)
+
+ testsuite = [ &
+ & new_unittest("valid-toml", test_valid_toml), &
+ & new_unittest("invalid-toml", test_invalid_toml, should_fail=.true.), &
+ & new_unittest("missing-file", test_missing_file, should_fail=.true.)]
+
+ end subroutine collect_toml
+
+
+ !> Try to read some unnecessary obscure and convoluted but not invalid package file
+ subroutine test_valid_toml(error)
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_table), allocatable :: table
+ character(len=*), parameter :: manifest = 'fpm-valid-toml.toml'
+ integer :: unit
+
+ open(file=manifest, newunit=unit)
+ write(unit, '(a)') &
+ & 'name = "example"', &
+ & '[dependencies.fpm]', &
+ & 'git = "https://github.com/fortran-lang/fpm"', &
+ & '[[executable]]', &
+ & 'name = "example-#1" # comment', &
+ & 'source-dir = "prog"', &
+ & '[dependencies]', &
+ & 'toml-f.git = "git@github.com:toml-f/toml-f.git"', &
+ & '"toml..f" = { path = ".." }', &
+ & '[["executable"]]', &
+ & 'name = "example-#2"', &
+ & 'source-dir = "prog"', &
+ & '[executable.dependencies]', &
+ & '[''library'']', &
+ & 'source-dir = """', &
+ & 'lib""" # comment'
+ close(unit)
+
+ call read_package_file(table, manifest, error)
+
+ open(file=manifest, newunit=unit)
+ close(unit, status='delete')
+
+ end subroutine test_valid_toml
+
+
+ !> Try to read an invalid TOML document
+ subroutine test_invalid_toml(error)
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_table), allocatable :: table
+ character(len=*), parameter :: manifest = 'fpm-invalid-toml.toml'
+ integer :: unit
+
+ open(file=manifest, newunit=unit)
+ write(unit, '(a)') &
+ & '# INVALID TOML DOC', &
+ & 'name = "example"', &
+ & 'dependencies.fpm.git = "https://github.com/fortran-lang/fpm"', &
+ & '[dependencies]', &
+ & 'toml-f.git = "git@github.com:toml-f/toml-f.git"', &
+ & '"toml..f" = { path = ".." }'
+ close(unit)
+
+ call read_package_file(table, manifest, error)
+
+ open(file=manifest, newunit=unit)
+ close(unit, status='delete')
+
+ end subroutine test_invalid_toml
+
+
+ !> Try to read configuration from a non-existing file
+ subroutine test_missing_file(error)
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_table), allocatable :: table
+
+ call read_package_file(table, 'low+chance+of+existing.toml', error)
+
+ end subroutine test_missing_file
+
+
+end module test_toml
diff --git a/fpm/test/testsuite.f90 b/fpm/test/testsuite.f90
new file mode 100644
index 0000000..bd0d415
--- /dev/null
+++ b/fpm/test/testsuite.f90
@@ -0,0 +1,122 @@
+!> Define some procedures to automate collecting and launching of tests
+module testsuite
+ use fpm_error, only : error_t, test_failed => fatal_error
+ implicit none
+ private
+
+ public :: run_testsuite, new_unittest, test_failed
+ public :: unittest_t, error_t
+
+
+ abstract interface
+ !> Entry point for tests
+ subroutine test_interface(error)
+ import :: error_t
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ end subroutine test_interface
+ end interface
+
+
+ !> Declaration of a unit test
+ type :: unittest_t
+
+ !> Name of the test
+ character(len=:), allocatable :: name
+
+ !> Entry point of the test
+ procedure(test_interface), pointer, nopass :: test => null()
+
+ !> Whether test is supposed to fail
+ logical :: should_fail = .false.
+
+ end type unittest_t
+
+
+ abstract interface
+ !> Collect all tests
+ subroutine collect_interface(testsuite)
+ import :: unittest_t
+
+ !> Collection of tests
+ type(unittest_t), allocatable, intent(out) :: testsuite(:)
+
+ end subroutine collect_interface
+ end interface
+
+
+contains
+
+
+ !> Driver for testsuite
+ subroutine run_testsuite(collect, unit, stat)
+
+ !> Collect tests
+ procedure(collect_interface) :: collect
+
+ !> Unit for IO
+ integer, intent(in) :: unit
+
+ !> Number of failed tests
+ integer, intent(out) :: stat
+
+ type(unittest_t), allocatable :: testsuite(:)
+ character(len=*), parameter :: fmt = '("#", *(1x, a))'
+ character(len=*), parameter :: indent = repeat(" ", 5) // repeat(".", 3)
+ type(error_t), allocatable :: error
+ integer :: ii
+
+ stat = 0
+
+ call collect(testsuite)
+
+ do ii = 1, size(testsuite)
+ write(unit, '("#", *(1x, a))') "Starting", testsuite(ii)%name, "..."
+ call testsuite(ii)%test(error)
+ if (allocated(error) .neqv. testsuite(ii)%should_fail) then
+ if (testsuite(ii)%should_fail) then
+ write(unit, fmt) indent, testsuite(ii)%name, "[UNEXPECTED PASS]"
+ else
+ write(unit, fmt) indent, testsuite(ii)%name, "[FAILED]"
+ end if
+ stat = stat + 1
+ else
+ if (testsuite(ii)%should_fail) then
+ write(unit, fmt) indent, testsuite(ii)%name, "[EXPECTED FAIL]"
+ else
+ write(unit, fmt) indent, testsuite(ii)%name, "[PASSED]"
+ end if
+ end if
+ if (allocated(error)) then
+ write(unit, '(a)') error%message
+ end if
+ end do
+
+ end subroutine run_testsuite
+
+
+ !> Register a new unit test
+ function new_unittest(name, test, should_fail) result(self)
+
+ !> Name of the test
+ character(len=*), intent(in) :: name
+
+ !> Entry point for the test
+ procedure(test_interface) :: test
+
+ !> Whether test is supposed to error or not
+ logical, intent(in), optional :: should_fail
+
+ !> Newly registered test
+ type(unittest_t) :: self
+
+ self%name = name
+ self%test => test
+ if (present(should_fail)) self%should_fail = should_fail
+
+ end function new_unittest
+
+
+end module testsuite