aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--fpm/app/main.f904
-rw-r--r--fpm/fpm.toml2
-rw-r--r--fpm/src/fpm.f90153
-rw-r--r--fpm/src/fpm/cmd/update.f9068
-rw-r--r--fpm/src/fpm/dependency.f90811
-rw-r--r--fpm/src/fpm/git.f9061
-rw-r--r--fpm/src/fpm/manifest.f9022
-rw-r--r--fpm/src/fpm/toml.f904
-rw-r--r--fpm/src/fpm/versioning.f9020
-rw-r--r--fpm/src/fpm_command_line.f9052
-rw-r--r--fpm/src/fpm_filesystem.f9056
-rw-r--r--fpm/src/fpm_model.f904
-rw-r--r--fpm/test/fpm_test/main.f902
-rw-r--r--fpm/test/fpm_test/test_package_dependencies.f90240
-rw-r--r--fpm/test/help_test/help_test.f904
15 files changed, 1354 insertions, 149 deletions
diff --git a/fpm/app/main.f90 b/fpm/app/main.f90
index 28258ad..0fe159b 100644
--- a/fpm/app/main.f90
+++ b/fpm/app/main.f90
@@ -6,9 +6,11 @@ use fpm_command_line, only: &
fpm_run_settings, &
fpm_test_settings, &
fpm_install_settings, &
+ fpm_update_settings, &
get_command_line_settings
use fpm, only: cmd_build, cmd_install, cmd_run
use fpm_cmd_new, only: cmd_new
+use fpm_cmd_update, only : cmd_update
implicit none
@@ -27,6 +29,8 @@ type is (fpm_test_settings)
call cmd_run(settings,test=.true.)
type is (fpm_install_settings)
call cmd_install(settings)
+type is (fpm_update_settings)
+ call cmd_update(settings)
end select
end program main
diff --git a/fpm/fpm.toml b/fpm/fpm.toml
index 70c9603..3179d2d 100644
--- a/fpm/fpm.toml
+++ b/fpm/fpm.toml
@@ -8,7 +8,7 @@ copyright = "2020 fpm contributors"
[dependencies]
[dependencies.toml-f]
git = "https://github.com/toml-f/toml-f"
-tag = "v0.2.1"
+rev = "2f5eaba864ff630ba0c3791126a3f811b6e437f3"
[dependencies.M_CLI2]
git = "https://github.com/urbanjost/M_CLI2.git"
diff --git a/fpm/src/fpm.f90 b/fpm/src/fpm.f90
index 9821191..f23e119 100644
--- a/fpm/src/fpm.f90
+++ b/fpm/src/fpm.f90
@@ -3,6 +3,7 @@ use fpm_strings, only: string_t, str_ends_with, operator(.in.)
use fpm_backend, only: build_package
use fpm_command_line, only: fpm_build_settings, fpm_new_settings, &
fpm_run_settings, fpm_install_settings, fpm_test_settings
+use fpm_dependency, only : new_dependency_tree
use fpm_environment, only: run
use fpm_filesystem, only: is_dir, join_path, number_of_rows, list_files, exists, basename
use fpm_model, only: fpm_model_t, srcfile_t, build_target_t, &
@@ -29,125 +30,6 @@ public :: build_model, cmd_build, cmd_install, cmd_run
contains
-recursive subroutine add_libsources_from_package(sources,link_libraries,package_list,package, &
- package_root,dev_depends,error)
- ! Discover library sources in a package, recursively including dependencies
- !
- type(srcfile_t), allocatable, intent(inout), target :: sources(:)
- type(string_t), allocatable, intent(inout) :: link_libraries(:)
- type(string_t), allocatable, intent(inout) :: package_list(:)
- type(package_config_t), intent(in) :: package
- character(*), intent(in) :: package_root
- logical, intent(in) :: dev_depends
- type(error_t), allocatable, intent(out) :: error
-
- ! Add package library sources
- if (allocated(package%library)) then
-
- call add_sources_from_dir(sources, join_path(package_root,package%library%source_dir), &
- FPM_SCOPE_LIB, error=error)
-
- if (allocated(error)) then
- return
- end if
-
- end if
-
- ! Add library sources from dependencies
- if (allocated(package%dependency)) then
-
- call add_dependencies(package%dependency)
-
- if (allocated(error)) then
- return
- end if
-
- end if
-
- ! Add library sources from dev-dependencies
- if (dev_depends .and. allocated(package%dev_dependency)) then
-
- call add_dependencies(package%dev_dependency)
-
- if (allocated(error)) then
- return
- end if
-
- end if
-
- contains
-
- subroutine add_dependencies(dependency_list)
- type(dependency_config_t), intent(in) :: dependency_list(:)
-
- integer :: i
- type(string_t) :: dep_name
- type(package_config_t) :: dependency
-
- character(:), allocatable :: dependency_path
-
- do i=1,size(dependency_list)
-
- if (dependency_list(i)%name .in. package_list) then
- cycle
- end if
-
- if (allocated(dependency_list(i)%git)) then
-
- dependency_path = join_path('build','dependencies',dependency_list(i)%name)
-
- if (.not.exists(join_path(dependency_path,'fpm.toml'))) then
- call dependency_list(i)%git%checkout(dependency_path, error)
- if (allocated(error)) return
- end if
-
- else if (allocated(dependency_list(i)%path)) then
-
- dependency_path = join_path(package_root,dependency_list(i)%path)
-
- end if
-
- call get_package_data(dependency, &
- join_path(dependency_path,"fpm.toml"), error)
-
- if (allocated(error)) then
- error%message = 'Error while parsing manifest for dependency package at:'//&
- new_line('a')//join_path(dependency_path,"fpm.toml")//&
- new_line('a')//error%message
- return
- end if
-
- if (.not.allocated(dependency%library) .and. &
- exists(join_path(dependency_path,"src"))) then
- allocate(dependency%library)
- dependency%library%source_dir = "src"
- end if
-
-
- call add_libsources_from_package(sources,link_libraries,package_list,dependency, &
- package_root=dependency_path, &
- dev_depends=.false., error=error)
-
- if (allocated(error)) then
- error%message = 'Error while processing sources for dependency package "'//&
- new_line('a')//dependency%name//'"'//&
- new_line('a')//error%message
- return
- end if
-
- dep_name%s = dependency_list(i)%name
- package_list = [package_list, dep_name]
- if (allocated(dependency%build%link)) then
- link_libraries = [link_libraries, dependency%build%link]
- end if
-
- end do
-
- end subroutine add_dependencies
-
-end subroutine add_libsources_from_package
-
-
subroutine build_model(model, settings, package, error)
! Constructs a valid fpm model from command line settings and toml manifest
!
@@ -158,6 +40,8 @@ subroutine build_model(model, settings, package, error)
type(string_t), allocatable :: package_list(:)
integer :: i
+ type(package_config_t) :: dependency
+ character(len=:), allocatable :: manifest, lib_dir
if(settings%verbose)then
write(*,*)'<INFO>BUILD_NAME:',settings%build_name
@@ -172,6 +56,10 @@ subroutine build_model(model, settings, package, error)
allocate(model%link_libraries(0))
end if
+ call new_dependency_tree(model%deps, cache=join_path("build", "cache.toml"))
+ call model%deps%add(package, error)
+ if (allocated(error)) return
+
allocate(package_list(1))
package_list(1)%s = package%name
@@ -228,12 +116,27 @@ subroutine build_model(model, settings, package, error)
endif
- ! Add library sources, including local dependencies
- call add_libsources_from_package(model%sources,model%link_libraries,package_list,package, &
- package_root='.',dev_depends=.true.,error=error)
- if (allocated(error)) then
- return
- end if
+ do i = 1, model%deps%ndep
+ associate(dep => model%deps%dep(i))
+ manifest = join_path(dep%proj_dir, "fpm.toml")
+
+ call get_package_data(dependency, manifest, error, &
+ apply_defaults=.true.)
+ if (allocated(error)) exit
+
+ if (allocated(dependency%library)) then
+ lib_dir = join_path(dep%proj_dir, dependency%library%source_dir)
+ call add_sources_from_dir(model%sources, lib_dir, FPM_SCOPE_LIB, &
+ error=error)
+ if (allocated(error)) exit
+ end if
+
+ if (allocated(dependency%build%link)) then
+ model%link_libraries = [model%link_libraries, dependency%build%link]
+ end if
+ end associate
+ end do
+ if (allocated(error)) return
call targets_from_sources(model,model%sources)
diff --git a/fpm/src/fpm/cmd/update.f90 b/fpm/src/fpm/cmd/update.f90
new file mode 100644
index 0000000..d7cc549
--- /dev/null
+++ b/fpm/src/fpm/cmd/update.f90
@@ -0,0 +1,68 @@
+module fpm_cmd_update
+ use fpm_command_line, only : fpm_update_settings
+ use fpm_dependency, only : dependency_tree_t, new_dependency_tree
+ use fpm_error, only : error_t
+ use fpm_filesystem, only : exists, mkdir, join_path, delete_file
+ use fpm_manifest, only : package_config_t, get_package_data
+ implicit none
+ private
+ public :: cmd_update
+
+contains
+
+ !> Entry point for the update subcommand
+ subroutine cmd_update(settings)
+ !> Representation of the command line arguments
+ type(fpm_update_settings), intent(in) :: settings
+ type(package_config_t) :: package
+ type(dependency_tree_t) :: deps
+ type(error_t), allocatable :: error
+
+ integer :: ii
+ character(len=:), allocatable :: cache
+
+ call get_package_data(package, "fpm.toml", error, apply_defaults=.true.)
+ call handle_error(error)
+
+ if (.not.exists("build")) then
+ call mkdir("build")
+ end if
+
+ cache = join_path("build", "cache.toml")
+ if (settings%clean) then
+ call delete_file(cache)
+ end if
+
+ call new_dependency_tree(deps, cache=cache, &
+ verbosity=merge(2, 1, settings%verbose))
+
+ call deps%add(package, error)
+ call handle_error(error)
+
+ if (settings%fetch_only) return
+
+ if (size(settings%name) == 0) then
+ do ii = 1, deps%ndep
+ call deps%update(deps%dep(ii)%name, error)
+ call handle_error(error)
+ end do
+ else
+ do ii = 1, size(settings%name)
+ call deps%update(trim(settings%name(ii)), error)
+ call handle_error(error)
+ end do
+ end if
+
+ end subroutine cmd_update
+
+ !> Error handling for this command
+ subroutine handle_error(error)
+ !> Potential error
+ type(error_t), intent(in), optional :: error
+ if (present(error)) then
+ print '(a)', error%message
+ error stop 1
+ end if
+ end subroutine handle_error
+
+end module fpm_cmd_update
diff --git a/fpm/src/fpm/dependency.f90 b/fpm/src/fpm/dependency.f90
new file mode 100644
index 0000000..e5e18ae
--- /dev/null
+++ b/fpm/src/fpm/dependency.f90
@@ -0,0 +1,811 @@
+!> # Dependency management
+!>
+!> ## Fetching dependencies and creating a dependency tree
+!>
+!> Dependencies on the top-level can be specified from:
+!>
+!> - `package%dependencies`
+!> - `package%dev_dependencies`
+!> - `package%executable(:)%dependencies`
+!> - `package%test(:)%dependencies`
+!>
+!> Each dependency is fetched in some way and provides a path to its package
+!> manifest.
+!> The `package%dependencies` of the dependencies are resolved recursively.
+!>
+!> To initialize the dependency tree all dependencies are recursively fetched
+!> and stored in a flat data structure to avoid retrieving a package twice.
+!> The data structure used to store this information should describe the current
+!> status of the dependency tree. Important information are:
+!>
+!> - name of the package
+!> - version of the package
+!> - path to the package root
+!>
+!> Additionally, for version controlled dependencies the following should be
+!> stored along with the package:
+!>
+!> - the upstream url
+!> - the current checked out revision
+!>
+!> Fetching a remote (version controlled) dependency turns it for our purpose
+!> into a local path dependency which is handled by the same means.
+!>
+!> ## Updating dependencies
+!>
+!> For a given dependency tree all top-level dependencies can be updated.
+!> We have two cases to consider, a remote dependency and a local dependency,
+!> again, remote dependencies turn into local dependencies by fetching.
+!> Therefore we will update remote dependencies by simply refetching them.
+!>
+!> For remote dependencies we have to refetch if the revision in the manifest
+!> changes or the upstream HEAD has changed (for branches _and_ tags).
+!>
+!> @Note For our purpose a tag is just a fancy branch name. Tags can be delete and
+!> modified afterwards, therefore they do not differ too much from branches
+!> from our perspective.
+!>
+!> For the latter case we only know if we actually fetch from the upstream URL.
+!>
+!> In case of local (and fetched remote) dependencies we have to read the package
+!> manifest and compare its dependencies against our dependency tree, any change
+!> requires updating the respective dependencies as well.
+!>
+!> ## Handling dependency compatibilties
+!>
+!> Currenly ignored. First come, first serve.
+module fpm_dependency
+ use, intrinsic :: iso_fortran_env, only : output_unit
+ use fpm_environment, only : get_os_type, OS_WINDOWS
+ use fpm_error, only : error_t, fatal_error
+ use fpm_filesystem, only : exists, join_path, mkdir, canon_path, windows_path
+ use fpm_git, only : git_target_revision, git_target_default, git_revision
+ use fpm_manifest, only : package_config_t, dependency_config_t, &
+ get_package_data
+ use fpm_strings, only : string_t, operator(.in.)
+ use fpm_toml, only : toml_table, toml_key, toml_error, toml_serializer, &
+ toml_parse, get_value, set_value, add_table
+ use fpm_versioning, only : version_t, new_version, char
+ implicit none
+ private
+
+ public :: dependency_tree_t, new_dependency_tree
+ public :: dependency_node_t, new_dependency_node
+ public :: resize
+
+
+ !> Overloaded reallocation interface
+ interface resize
+ module procedure :: resize_dependency_node
+ end interface resize
+
+
+ !> Dependency node in the projects dependency tree
+ type, extends(dependency_config_t) :: dependency_node_t
+ !> Actual version of this dependency
+ type(version_t), allocatable :: version
+ !> Installation prefix of this dependencies
+ character(len=:), allocatable :: proj_dir
+ !> Checked out revision of the version control system
+ character(len=:), allocatable :: revision
+ !> Dependency is handled
+ logical :: done = .false.
+ !> Dependency should be updated
+ logical :: update = .false.
+ contains
+ !> Update dependency from project manifest
+ procedure :: register
+ end type dependency_node_t
+
+
+ !> Respresentation of a projects dependencies
+ !>
+ !> The dependencies are stored in a simple array for now, this can be replaced
+ !> with a binary-search tree or a hash table in the future.
+ type :: dependency_tree_t
+ !> Unit for IO
+ integer :: unit = output_unit
+ !> Verbosity of printout
+ integer :: verbosity = 1
+ !> Installation prefix for dependencies
+ character(len=:), allocatable :: dep_dir
+ !> Number of currently registered dependencies
+ integer :: ndep = 0
+ !> Flattend list of all dependencies
+ type(dependency_node_t), allocatable :: dep(:)
+ !> Cache file
+ character(len=:), allocatable :: cache
+ contains
+ !> Overload procedure to add new dependencies to the tree
+ generic :: add => add_project, add_project_dependencies, add_dependencies, &
+ add_dependency
+ !> Main entry point to add a project
+ procedure, private :: add_project
+ !> Add a project and its dependencies to the dependency tree
+ procedure, private :: add_project_dependencies
+ !> Add a list of dependencies to the dependency tree
+ procedure, private :: add_dependencies
+ !> Add a single dependency to the dependency tree
+ procedure, private :: add_dependency
+ !> Resolve dependencies
+ generic :: resolve => resolve_dependencies, resolve_dependency
+ !> Resolve dependencies
+ procedure, private :: resolve_dependencies
+ !> Resolve dependencies
+ procedure, private :: resolve_dependency
+ !> Find a dependency in the tree
+ generic :: find => find_dependency, find_name
+ !> Find a dependency from an dependency configuration
+ procedure, private :: find_dependency
+ !> Find a dependency by its name
+ procedure, private :: find_name
+ !> Depedendncy resolution finished
+ procedure :: finished
+ !> Reading of dependency tree
+ generic :: load => load_from_file, load_from_unit, load_from_toml
+ !> Read dependency tree from file
+ procedure, private :: load_from_file
+ !> Read dependency tree from formatted unit
+ procedure, private :: load_from_unit
+ !> Read dependency tree from TOML data structure
+ procedure, private :: load_from_toml
+ !> Writing of dependency tree
+ generic :: dump => dump_to_file, dump_to_unit, dump_to_toml
+ !> Write dependency tree to file
+ procedure, private :: dump_to_file
+ !> Write dependency tree to formatted unit
+ procedure, private :: dump_to_unit
+ !> Write dependency tree to TOML data structure
+ procedure, private :: dump_to_toml
+ !> Update dependency tree
+ generic :: update => update_dependency
+ !> Update a list of dependencies
+ procedure, private :: update_dependency
+ end type dependency_tree_t
+
+ !> Common output format for writing to the command line
+ character(len=*), parameter :: out_fmt = '("#", *(1x, g0))'
+
+contains
+
+ !> Create a new dependency tree
+ subroutine new_dependency_tree(self, verbosity, cache)
+ !> Instance of the dependency tree
+ type(dependency_tree_t), intent(out) :: self
+ !> Verbosity of printout
+ integer, intent(in), optional :: verbosity
+ !> Name of the cache file
+ character(len=*), intent(in), optional :: cache
+
+ call resize(self%dep)
+ self%dep_dir = join_path("build", "dependencies")
+
+ if (present(verbosity)) then
+ self%verbosity = verbosity
+ end if
+
+ if (present(cache)) then
+ self%cache = cache
+ end if
+
+ end subroutine new_dependency_tree
+
+ !> Create a new dependency node from a configuration
+ pure subroutine new_dependency_node(self, dependency, version, proj_dir, update)
+ !> Instance of the dependency node
+ type(dependency_node_t), intent(out) :: self
+ !> Dependency configuration data
+ type(dependency_config_t), intent(in) :: dependency
+ !> Version of the dependency
+ type(version_t), intent(in), optional :: version
+ !> Installation prefix of the dependency
+ character(len=*), intent(in), optional :: proj_dir
+ !> Dependency should be updated
+ logical, intent(in), optional :: update
+
+ self%dependency_config_t = dependency
+
+ if (present(version)) then
+ self%version = version
+ end if
+
+ if (present(proj_dir)) then
+ self%proj_dir = proj_dir
+ end if
+
+ if (present(update)) then
+ self%update = update
+ end if
+
+ end subroutine new_dependency_node
+
+ !> Add project dependencies, each depth level after each other.
+ !>
+ !> We implement this algorithm in an interative rather than a recursive fashion
+ !> as a choice of design.
+ subroutine add_project(self, package, error)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(inout) :: self
+ !> Project configuration to add
+ type(package_config_t), intent(in) :: package
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(dependency_config_t) :: dependency
+ character(len=:), allocatable :: root
+ logical :: main
+
+ if (allocated(self%cache)) then
+ call self%load(self%cache, error)
+ if (allocated(error)) return
+ end if
+
+ if (.not.exists(self%dep_dir)) then
+ call mkdir(self%dep_dir)
+ end if
+
+ root = "."
+
+ ! Create this project as the first dependency node (depth 0)
+ dependency%name = package%name
+ dependency%path = root
+ call self%add(dependency, error)
+ if (allocated(error)) return
+
+ ! Resolve the root project
+ call self%resolve(root, error)
+ if (allocated(error)) return
+
+ ! Add the root project dependencies (depth 1)
+ call self%add(package, root, .true., error)
+ if (allocated(error)) return
+
+ ! Now decent into the dependency tree, level for level
+ do while(.not.self%finished())
+ call self%resolve(root, error)
+ if (allocated(error)) exit
+ end do
+ if (allocated(error)) return
+
+ if (allocated(self%cache)) then
+ call self%dump(self%cache, error)
+ if (allocated(error)) return
+ end if
+
+ end subroutine add_project
+
+ !> Add a project and its dependencies to the dependency tree
+ recursive subroutine add_project_dependencies(self, package, root, main, error)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(inout) :: self
+ !> Project configuration to add
+ type(package_config_t), intent(in) :: package
+ !> Current project root directory
+ character(len=*), intent(in) :: root
+ !> Is the main project
+ logical, intent(in) :: main
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: ii
+
+ if (allocated(package%dependency)) then
+ call self%add(package%dependency, error)
+ if (allocated(error)) return
+ end if
+
+ if (main) then
+ if (allocated(package%dev_dependency)) then
+ call self%add(package%dev_dependency, error)
+ if (allocated(error)) return
+ end if
+
+ if (allocated(package%executable)) then
+ do ii = 1, size(package%executable)
+ if (allocated(package%executable(ii)%dependency)) then
+ call self%add(package%executable(ii)%dependency, error)
+ if (allocated(error)) exit
+ end if
+ end do
+ if (allocated(error)) return
+ end if
+
+ if (allocated(package%test)) then
+ do ii = 1, size(package%test)
+ if (allocated(package%test(ii)%dependency)) then
+ call self%add(package%test(ii)%dependency, error)
+ if (allocated(error)) exit
+ end if
+ end do
+ if (allocated(error)) return
+ end if
+ end if
+
+ end subroutine add_project_dependencies
+
+ !> Add a list of dependencies to the dependency tree
+ subroutine add_dependencies(self, dependency, error)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(inout) :: self
+ !> Dependency configuration to add
+ type(dependency_config_t), intent(in) :: dependency(:)
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: ii, ndep
+
+ ndep = size(self%dep)
+ if (ndep < size(dependency) + self%ndep) then
+ call resize(self%dep, ndep + ndep/2 + size(dependency))
+ end if
+
+ do ii = 1, size(dependency)
+ call self%add(dependency(ii), error)
+ if (allocated(error)) exit
+ end do
+ if (allocated(error)) return
+
+ end subroutine add_dependencies
+
+ !> Add a single dependency to the dependency tree
+ pure subroutine add_dependency(self, dependency, error)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(inout) :: self
+ !> Dependency configuration to add
+ type(dependency_config_t), intent(in) :: dependency
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: id
+
+ id = self%find(dependency)
+ if (id == 0) then
+ self%ndep = self%ndep + 1
+ call new_dependency_node(self%dep(self%ndep), dependency)
+ end if
+
+ end subroutine add_dependency
+
+ !> Update dependency tree
+ subroutine update_dependency(self, name, error)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(inout) :: self
+ !> Name of the dependency to update
+ character(len=*), intent(in) :: name
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: id
+ type(package_config_t) :: package
+ character(len=:), allocatable :: manifest, proj_dir, revision, root
+
+ id = self%find(name)
+ root = "."
+
+ if (id <= 0) then
+ call fatal_error(error, "Cannot update dependency '"//name//"'")
+ return
+ end if
+
+ associate(dep => self%dep(id))
+ if (allocated(dep%git) .and. dep%update) then
+ if (self%verbosity > 1) then
+ write(self%unit, out_fmt) "Update:", dep%name
+ end if
+ proj_dir = join_path(self%dep_dir, dep%name)
+ call dep%git%checkout(proj_dir, error)
+ if (allocated(error)) return
+
+ ! Unset dependency and remove updatable attribute
+ dep%done = .false.
+ dep%update = .false.
+
+ ! Now decent into the dependency tree, level for level
+ do while(.not.self%finished())
+ call self%resolve(root, error)
+ if (allocated(error)) exit
+ end do
+ if (allocated(error)) return
+ end if
+ end associate
+
+ end subroutine update_dependency
+
+ !> Resolve all dependencies in the tree
+ subroutine resolve_dependencies(self, root, error)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(inout) :: self
+ !> Current installation prefix
+ character(len=*), intent(in) :: root
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: ii
+
+ do ii = 1, self%ndep
+ call self%resolve(self%dep(ii), root, error)
+ if (allocated(error)) exit
+ end do
+
+ if (allocated(error)) return
+
+ end subroutine resolve_dependencies
+
+ !> Resolve a single dependency node
+ subroutine resolve_dependency(self, dependency, root, error)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(inout) :: self
+ !> Dependency configuration to add
+ type(dependency_node_t), intent(inout) :: dependency
+ !> Current installation prefix
+ character(len=*), intent(in) :: root
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(package_config_t) :: package
+ character(len=:), allocatable :: manifest, proj_dir, revision
+ logical :: fetch
+
+ if (dependency%done) return
+
+ fetch = .false.
+ if (allocated(dependency%proj_dir)) then
+ proj_dir = dependency%proj_dir
+ else
+ if (allocated(dependency%path)) then
+ proj_dir = join_path(root, dependency%path)
+ else if (allocated(dependency%git)) then
+ proj_dir = join_path(self%dep_dir, dependency%name)
+ fetch = .not.exists(proj_dir)
+ if (fetch) then
+ call dependency%git%checkout(proj_dir, error)
+ if (allocated(error)) return
+ end if
+
+ end if
+ end if
+
+ if (allocated(dependency%git)) then
+ call git_revision(proj_dir, revision, error)
+ if (allocated(error)) return
+ end if
+
+ manifest = join_path(proj_dir, "fpm.toml")
+ call get_package_data(package, manifest, error)
+ if (allocated(error)) return
+
+ call dependency%register(package, proj_dir, fetch, revision, error)
+ if (allocated(error)) return
+
+ if (self%verbosity > 1) then
+ write(self%unit, out_fmt) &
+ "Dep:", dependency%name, "version", char(dependency%version), &
+ "at", dependency%proj_dir
+ end if
+
+ call self%add(package, proj_dir, .false., error)
+ if (allocated(error)) return
+
+ end subroutine resolve_dependency
+
+ !> Find a dependency in the dependency tree
+ pure function find_dependency(self, dependency) result(pos)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(in) :: self
+ !> Dependency configuration to add
+ class(dependency_config_t), intent(in) :: dependency
+ !> Index of the dependency
+ integer :: pos
+
+ integer :: ii
+
+ pos = self%find(dependency%name)
+
+ end function find_dependency
+
+ !> Find a dependency in the dependency tree
+ pure function find_name(self, name) result(pos)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(in) :: self
+ !> Dependency configuration to add
+ character(len=*), intent(in) :: name
+ !> Index of the dependency
+ integer :: pos
+
+ integer :: ii
+
+ pos = 0
+ do ii = 1, self%ndep
+ if (name == self%dep(ii)%name) then
+ pos = ii
+ exit
+ end if
+ end do
+
+ end function find_name
+
+ !> Check if we are done with the dependency resolution
+ pure function finished(self)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(in) :: self
+ !> All dependencies are updated
+ logical :: finished
+ integer :: ii
+
+ finished = all(self%dep(:self%ndep)%done)
+
+ end function finished
+
+ !> Update dependency from project manifest
+ subroutine register(self, package, root, fetch, revision, error)
+ !> Instance of the dependency node
+ class(dependency_node_t), intent(inout) :: self
+ !> Package configuration data
+ type(package_config_t), intent(in) :: package
+ !> Project has been fetched
+ logical, intent(in) :: fetch
+ !> Root directory of the project
+ character(len=*), intent(in) :: root
+ !> Git revision of the project
+ character(len=*), intent(in), optional :: revision
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ character(len=:), allocatable :: url
+ logical :: update
+
+ update = .false.
+ if (self%name /= package%name) then
+ call fatal_error(error, "Dependency name '"//package%name// &
+ & "' found, but expected '"//self%name//"' instead")
+ end if
+
+ self%version = package%version
+ self%proj_dir = root
+
+ if (allocated(self%git).and.present(revision)) then
+ self%revision = revision
+ if (.not.fetch) then
+ ! git object is HEAD always allows an update
+ update = .not.allocated(self%git%object)
+ if (.not.update) then
+ ! allow update in case the revision does not match the requested object
+ update = revision /= self%git%object
+ end if
+ end if
+ end if
+
+ self%update = update
+ self%done = .true.
+
+ end subroutine register
+
+ !> Read dependency tree from file
+ subroutine load_from_file(self, file, error)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(inout) :: self
+ !> File name
+ character(len=*), intent(in) :: file
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: unit
+ logical :: exist
+
+ inquire(file=file, exist=exist)
+ if (.not.exist) return
+
+ open(file=file, newunit=unit)
+ call self%load(unit, error)
+ close(unit)
+ end subroutine load_from_file
+
+ !> Read dependency tree from file
+ subroutine load_from_unit(self, unit, error)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(inout) :: self
+ !> File name
+ integer, intent(in) :: unit
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_error), allocatable :: parse_error
+ type(toml_table), allocatable :: table
+
+ call toml_parse(table, unit, parse_error)
+
+ if (allocated(parse_error)) then
+ allocate(error)
+ call move_alloc(parse_error%message, error%message)
+ return
+ end if
+
+ call self%load(table, error)
+ if (allocated(error)) return
+
+ end subroutine load_from_unit
+
+ !> Read dependency tree from TOML data structure
+ subroutine load_from_toml(self, table, error)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(inout) :: self
+ !> Data structure
+ type(toml_table), intent(inout) :: table
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: ndep, ii
+ logical :: unix
+ character(len=:), allocatable :: version, url, obj, rev, proj_dir
+ type(toml_key), allocatable :: list(:)
+ type(toml_table), pointer :: ptr
+ type(dependency_config_t) :: dep
+
+ call table%get_keys(list)
+
+ ndep = size(self%dep)
+ if (ndep < size(list) + self%ndep) then
+ call resize(self%dep, ndep + ndep/2 + size(list))
+ end if
+
+ unix = get_os_type() /= OS_WINDOWS
+
+ do ii = 1, size(list)
+ call get_value(table, list(ii)%key, ptr)
+ call get_value(ptr, "version", version)
+ call get_value(ptr, "proj-dir", proj_dir)
+ call get_value(ptr, "git", url)
+ call get_value(ptr, "obj", obj)
+ call get_value(ptr, "rev", rev)
+ if (.not.allocated(proj_dir)) cycle
+ self%ndep = self%ndep + 1
+ associate(dep => self%dep(self%ndep))
+ dep%name = list(ii)%key
+ if (unix) then
+ dep%proj_dir = proj_dir
+ else
+ dep%proj_dir = windows_path(proj_dir)
+ end if
+ dep%done = .false.
+ if (allocated(version)) then
+ if (.not.allocated(dep%version)) allocate(dep%version)
+ call new_version(dep%version, version, error)
+ if (allocated(error)) exit
+ end if
+ if (allocated(version)) then
+ call new_version(dep%version, version, error)
+ if (allocated(error)) exit
+ end if
+ if (allocated(url)) then
+ if (allocated(obj)) then
+ dep%git = git_target_revision(url, obj)
+ else
+ dep%git = git_target_default(url)
+ end if
+ if (allocated(rev)) then
+ dep%revision = rev
+ end if
+ else
+ dep%path = proj_dir
+ end if
+ end associate
+ end do
+ if (allocated(error)) return
+
+ self%ndep = size(list)
+ end subroutine load_from_toml
+
+ !> Write dependency tree to file
+ subroutine dump_to_file(self, file, error)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(inout) :: self
+ !> File name
+ character(len=*), intent(in) :: file
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: unit
+
+ open(file=file, newunit=unit)
+ call self%dump(unit, error)
+ close(unit)
+ if (allocated(error)) return
+
+ end subroutine dump_to_file
+
+ !> Write dependency tree to file
+ subroutine dump_to_unit(self, unit, error)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(inout) :: self
+ !> Formatted unit
+ integer, intent(in) :: unit
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_table) :: table
+ type(toml_serializer) :: ser
+
+ table = toml_table()
+ ser = toml_serializer(unit)
+
+ call self%dump(table, error)
+
+ call table%accept(ser)
+
+ end subroutine dump_to_unit
+
+ !> Write dependency tree to TOML datastructure
+ subroutine dump_to_toml(self, table, error)
+ !> Instance of the dependency tree
+ class(dependency_tree_t), intent(inout) :: self
+ !> Data structure
+ type(toml_table), intent(inout) :: table
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: ii
+ type(toml_table), pointer :: ptr
+ character(len=:), allocatable :: proj_dir
+
+ do ii = 1, self%ndep
+ associate(dep => self%dep(ii))
+ call add_table(table, dep%name, ptr)
+ if (.not.associated(ptr)) then
+ call fatal_error(error, "Cannot create entry for "//dep%name)
+ exit
+ end if
+ if (allocated(dep%version)) then
+ call set_value(ptr, "version", char(dep%version))
+ end if
+ proj_dir = canon_path(dep%proj_dir)
+ call set_value(ptr, "proj-dir", proj_dir)
+ if (allocated(dep%git)) then
+ call set_value(ptr, "git", dep%git%url)
+ if (allocated(dep%git%object)) then
+ call set_value(ptr, "obj", dep%git%object)
+ end if
+ if (allocated(dep%revision)) then
+ call set_value(ptr, "rev", dep%revision)
+ end if
+ end if
+ end associate
+ end do
+ if (allocated(error)) return
+
+ end subroutine dump_to_toml
+
+ !> Reallocate a list of dependencies
+ pure subroutine resize_dependency_node(var, n)
+ !> Instance of the array to be resized
+ type(dependency_node_t), allocatable, intent(inout) :: var(:)
+ !> Dimension of the final array size
+ integer, intent(in), optional :: n
+
+ type(dependency_node_t), allocatable :: tmp(:)
+ integer :: this_size, new_size
+ integer, parameter :: initial_size = 16
+
+ if (allocated(var)) then
+ this_size = size(var, 1)
+ call move_alloc(var, tmp)
+ else
+ this_size = initial_size
+ end if
+
+ if (present(n)) then
+ new_size = n
+ else
+ new_size = this_size + this_size/2 + 1
+ end if
+
+ allocate(var(new_size))
+
+ if (allocated(tmp)) then
+ this_size = min(size(tmp, 1), size(var, 1))
+ var(:this_size) = tmp(:this_size)
+ deallocate(tmp)
+ end if
+
+ end subroutine resize_dependency_node
+
+end module fpm_dependency
diff --git a/fpm/src/fpm/git.f90 b/fpm/src/fpm/git.f90
index 187b551..08e27b2 100644
--- a/fpm/src/fpm/git.f90
+++ b/fpm/src/fpm/git.f90
@@ -1,11 +1,13 @@
!> Implementation for interacting with git repositories.
module fpm_git
use fpm_error, only: error_t, fatal_error
+ use fpm_filesystem, only : get_temp_filename, getline
implicit none
public :: git_target_t
public :: git_target_default, git_target_branch, git_target_tag, &
& git_target_revision
+ public :: git_revision
!> Possible git target
@@ -31,10 +33,9 @@ module fpm_git
!> Description of an git target
type :: git_target_t
- private
!> Kind of the git target
- integer :: descriptor = git_descriptor%default
+ integer, private :: descriptor = git_descriptor%default
!> Target URL of the git repository
character(len=:), allocatable :: url
@@ -128,7 +129,7 @@ contains
end function git_target_tag
- subroutine checkout(self,local_path, error)
+ subroutine checkout(self, local_path, error)
!> Instance of the git target
class(git_target_t), intent(in) :: self
@@ -138,12 +139,9 @@ contains
!> Error
type(error_t), allocatable, intent(out) :: error
-
- !> git object ref
- character(:), allocatable :: object
- !> Stat for execute_command_line
integer :: stat
+ character(len=:), allocatable :: object
if (allocated(self%object)) then
object = self%object
@@ -158,8 +156,8 @@ contains
return
end if
- call execute_command_line("git -C "//local_path//" fetch "//self%url//&
- " "//object, exitstat=stat)
+ call execute_command_line("git -C "//local_path//" fetch --depth=1 "// &
+ self%url//" "//object, exitstat=stat)
if (stat /= 0) then
call fatal_error(error,'Error while fetching git repository for remote dependency')
@@ -173,7 +171,50 @@ contains
return
end if
- end subroutine checkout
+ end subroutine checkout
+
+
+ subroutine git_revision(local_path, object, error)
+
+ !> Local path to checkout in
+ character(*), intent(in) :: local_path
+
+ !> Git object reference
+ character(len=:), allocatable, intent(out) :: object
+
+ !> Error
+ type(error_t), allocatable, intent(out) :: error
+
+ integer :: stat, unit, istart, iend
+ character(len=:), allocatable :: temp_file, line, iomsg
+ character(len=*), parameter :: hexdigits = '0123456789abcdef'
+
+ allocate(temp_file, source=get_temp_filename())
+ line = "git -C "//local_path//" log -n 1 > "//temp_file
+ call execute_command_line(line, exitstat=stat)
+
+ if (stat /= 0) then
+ call fatal_error(error, "Error while retrieving commit information")
+ return
+ end if
+
+ open(file=temp_file, newunit=unit)
+ call getline(unit, line, stat, iomsg)
+
+ if (stat /= 0) then
+ call fatal_error(error, iomsg)
+ return
+ end if
+ close(unit, status="delete")
+
+ ! Tokenize:
+ ! commit 0123456789abcdef (HEAD, ...)
+ istart = scan(line, ' ') + 1
+ iend = verify(line(istart:), hexdigits) + istart - 1
+ if (iend < istart) iend = len(line)
+ object = line(istart:iend)
+
+ end subroutine git_revision
!> Show information on git target
diff --git a/fpm/src/fpm/manifest.f90 b/fpm/src/fpm/manifest.f90
index 00e57fe..2398d79 100644
--- a/fpm/src/fpm/manifest.f90
+++ b/fpm/src/fpm/manifest.f90
@@ -9,17 +9,18 @@
module fpm_manifest
use fpm_manifest_build, only: build_config_t
use fpm_manifest_executable, only : executable_config_t
+ use fpm_manifest_dependency, only : dependency_config_t
use fpm_manifest_library, only : library_config_t
use fpm_manifest_package, only : package_config_t, new_package
use fpm_error, only : error_t, fatal_error, file_not_found_error
use fpm_toml, only : toml_table, read_package_file
use fpm_manifest_test, only : test_config_t
- use fpm_filesystem, only: join_path, exists
+ use fpm_filesystem, only: join_path, exists, dirname
implicit none
private
public :: get_package_data, default_executable, default_library, default_test
- public :: package_config_t
+ public :: package_config_t, dependency_config_t
contains
@@ -83,6 +84,7 @@ contains
logical, intent(in), optional :: apply_defaults
type(toml_table), allocatable :: table
+ character(len=:), allocatable :: root
call read_package_file(table, file, error)
if (allocated(error)) return
@@ -97,7 +99,9 @@ contains
if (present(apply_defaults)) then
if (apply_defaults) then
- call package_defaults(package, error)
+ root = dirname(file)
+ if (len_trim(root) == 0) root = "."
+ call package_defaults(package, root, error)
if (allocated(error)) return
end if
end if
@@ -106,30 +110,34 @@ contains
!> Apply package defaults
- subroutine package_defaults(package, error)
+ subroutine package_defaults(package, root, error)
!> Parsed package meta data
type(package_config_t), intent(inout) :: package
+ !> Current working directory
+ character(len=*), intent(in) :: root
+
!> Error status of the operation
type(error_t), allocatable, intent(out) :: error
! Populate library in case we find the default src directory
- if (.not.allocated(package%library) .and. exists("src")) then
+ if (.not.allocated(package%library) .and. &
+ & exists(join_path(root, "src"))) then
allocate(package%library)
call default_library(package%library)
end if
! Populate executable in case we find the default app
if (.not.allocated(package%executable) .and. &
- exists(join_path('app',"main.f90"))) then
+ & exists(join_path(root, "app", "main.f90"))) then
allocate(package%executable(1))
call default_executable(package%executable(1), package%name)
end if
! Populate test in case we find the default test directory
if (.not.allocated(package%test) .and. &
- exists(join_path("test","main.f90"))) then
+ & exists(join_path(root, "test", "main.f90"))) then
allocate(package%test(1))
call default_test(package%test(1), package%name)
endif
diff --git a/fpm/src/fpm/toml.f90 b/fpm/src/fpm/toml.f90
index 2e1d6d3..dbaafcb 100644
--- a/fpm/src/fpm/toml.f90
+++ b/fpm/src/fpm/toml.f90
@@ -16,13 +16,15 @@ module fpm_toml
use fpm_error, only : error_t, fatal_error, file_not_found_error
use fpm_strings, only : string_t
use tomlf, only : toml_table, toml_array, toml_key, toml_stat, get_value, &
- & set_value, toml_parse, toml_error, new_table, add_table, add_array, len
+ & set_value, toml_parse, toml_error, new_table, add_table, add_array, &
+ & toml_serializer, len
implicit none
private
public :: read_package_file
public :: toml_table, toml_array, toml_key, toml_stat, get_value, set_value
public :: new_table, add_table, add_array, len
+ public :: toml_error, toml_serializer, toml_parse
interface get_value
diff --git a/fpm/src/fpm/versioning.f90 b/fpm/src/fpm/versioning.f90
index 145427e..b24fc3c 100644
--- a/fpm/src/fpm/versioning.f90
+++ b/fpm/src/fpm/versioning.f90
@@ -4,7 +4,7 @@ module fpm_versioning
implicit none
private
- public :: version_t, new_version
+ public :: version_t, new_version, char
type :: version_t
@@ -47,6 +47,11 @@ module fpm_versioning
integer, parameter :: max_limit = 3
+ interface char
+ module procedure :: as_string
+ end interface char
+
+
interface new_version
module procedure :: new_version_from_string
module procedure :: new_version_from_int
@@ -245,6 +250,19 @@ contains
end subroutine to_string
+ function as_string(self) result(string)
+
+ !> Version number
+ class(version_t), intent(in) :: self
+
+ !> Character representation of the version
+ character(len=:), allocatable :: string
+
+ call self%to_string(string)
+
+ end function as_string
+
+
!> Check to version numbers for equality
elemental function equals(lhs, rhs) result(is_equal)
diff --git a/fpm/src/fpm_command_line.f90 b/fpm/src/fpm_command_line.f90
index 6741aaf..ca15916 100644
--- a/fpm/src/fpm_command_line.f90
+++ b/fpm/src/fpm_command_line.f90
@@ -41,6 +41,7 @@ public :: fpm_cmd_settings, &
fpm_new_settings, &
fpm_run_settings, &
fpm_test_settings, &
+ fpm_update_settings, &
get_command_line_settings
type, abstract :: fpm_cmd_settings
@@ -74,6 +75,13 @@ end type
type, extends(fpm_cmd_settings) :: fpm_install_settings
end type
+!> Settings for interacting and updating with project dependencies
+type, extends(fpm_cmd_settings) :: fpm_update_settings
+ character(len=ibug),allocatable :: name(:)
+ logical :: fetch_only
+ logical :: clean
+end type
+
character(len=:),allocatable :: name
character(len=:),allocatable :: os_type
character(len=ibug),allocatable :: names(:)
@@ -82,11 +90,11 @@ character(len=:),allocatable :: tnames(:)
character(len=:), allocatable :: version_text(:)
character(len=:), allocatable :: help_new(:), help_fpm(:), help_run(:), &
& help_test(:), help_build(:), help_usage(:), help_runner(:), &
- & help_text(:), help_install(:), help_help(:), &
+ & help_text(:), help_install(:), help_help(:), help_update(:), &
& help_list(:), help_list_dash(:), help_list_nodash(:)
character(len=20),parameter :: manual(*)=[ character(len=20) ::&
& ' ', 'fpm', 'new', 'build', 'run', &
-& 'test', 'runner', 'list', 'help', 'version' ]
+& 'test', 'runner', 'update','list', 'help', 'version' ]
character(len=:), allocatable :: val_runner, val_build, val_compiler
@@ -267,6 +275,8 @@ contains
help_text=[character(len=widest) :: help_text, help_runner]
case('list ' )
help_text=[character(len=widest) :: help_text, help_list]
+ case('update ' )
+ help_text=[character(len=widest) :: help_text, help_update]
case('help ' )
help_text=[character(len=widest) :: help_text, help_help]
case('version' )
@@ -329,6 +339,21 @@ contains
& runner=val_runner, &
& verbose=lget('verbose') )
+ case('update')
+ call set_args('--fetch-only F --verbose F --clean F', &
+ help_update, version_text)
+
+ if( size(unnamed) .gt. 1 )then
+ names=unnamed(2:)
+ else
+ names=[character(len=len(names)) :: ]
+ endif
+
+ allocate(fpm_update_settings :: cmd_settings)
+ cmd_settings=fpm_update_settings(name=names, &
+ fetch_only=lget('fetch-only'), verbose=lget('verbose'), &
+ clean=lget('clean'))
+
case default
call set_args('&
@@ -414,6 +439,7 @@ contains
' new Create a new Fortran package directory with sample files ', &
' run Run the local package application programs ', &
' test Run the test programs ', &
+ ' update Update and manage project dependencies ', &
' ', &
' Enter "fpm --list" for a brief list of subcommand options. Enter ', &
' "fpm --help" or "fpm SUBCOMMAND --help" for detailed descriptions. ', &
@@ -423,6 +449,7 @@ contains
' build [--compiler COMPILER_NAME] [--release] [--list] ', &
' help [NAME(s)] ', &
' new NAME [--lib|--src] [--app] [--test] [--backfill] ', &
+ ' update [NAME(s)] [--fetch-only] [--clean] [--verbose] ', &
' list [--list] ', &
' run [[--target] NAME(s)] [--release] [--runner "CMD"] [--list] ', &
' [--compiler COMPILER_NAME] [-- ARGS] ', &
@@ -522,6 +549,7 @@ contains
' ', &
' + build Compile the packages into the "build/" directory. ', &
' + new Create a new Fortran package directory with sample files. ', &
+ ' + update Update the project dependencies. ', &
' + run Run the local package binaries. defaults to all binaries for ', &
' that release. ', &
' + test Run the tests. ', &
@@ -532,6 +560,7 @@ contains
' ', &
' build [--release] [--list] [--compiler COMPILER_NAME] ', &
' new NAME [--lib|--src] [--app] [--test] [--backfill] ', &
+ ' update [NAME(s)] [--fetch-only] [--clean] ', &
' run|test [[--target] NAME(s)] [--release] [--list] ', &
' [--runner "CMD"] [--compiler COMPILER_NAME] [-- ARGS] ', &
' help [NAME(s)] ', &
@@ -832,6 +861,25 @@ contains
' ', &
' fpm test tst1 tst2 --release # run production version of two tests ', &
'' ]
+ help_update=[character(len=80) :: &
+ 'NAME', &
+ ' fpm-update(1) - manage project dependencies', &
+ '', &
+ 'SYNOPSIS', &
+ ' fpm update [--fetch-only] [--clean] [--verbose] [NAME(s)]', &
+ '', &
+ 'DESCRIPTION', &
+ ' Manage and update project dependencies. If no dependency names are', &
+ ' provided all the dependencies are updated automatically.', &
+ '', &
+ 'OPTIONS', &
+ ' --fetch-only Only fetch dependencies, do not update existing projects', &
+ ' --clean Do not use previous dependency cache', &
+ ' --verbose Show additional printout', &
+ '', &
+ 'SEE ALSO', &
+ ' The fpm(1) home page at https://github.com/fortran-lang/fpm', &
+ '' ]
help_install=[character(len=80) :: &
' fpm(1) subcommand "install" ', &
' ', &
diff --git a/fpm/src/fpm_filesystem.f90 b/fpm/src/fpm_filesystem.f90
index ce0867e..8f89243 100644
--- a/fpm/src/fpm_filesystem.f90
+++ b/fpm/src/fpm_filesystem.f90
@@ -6,7 +6,7 @@ module fpm_filesystem
implicit none
private
public :: basename, canon_path, dirname, is_dir, join_path, number_of_rows, read_lines, list_files,&
- mkdir, exists, get_temp_filename, windows_path
+ mkdir, exists, get_temp_filename, windows_path, getline, delete_file
integer, parameter :: LINE_BUFFER_LEN = 1000
@@ -390,4 +390,58 @@ function unix_path(path) result(nixpath)
end function unix_path
+
+subroutine getline(unit, line, iostat, iomsg)
+
+ !> Formatted IO unit
+ integer, intent(in) :: unit
+
+ !> Line to read
+ character(len=:), allocatable, intent(out) :: line
+
+ !> Status of operation
+ integer, intent(out) :: iostat
+
+ !> Error message
+ character(len=:), allocatable, optional :: iomsg
+
+ character(len=LINE_BUFFER_LEN) :: buffer
+ character(len=LINE_BUFFER_LEN) :: msg
+ integer :: size
+ integer :: stat
+
+ allocate(character(len=0) :: line)
+ do
+ read(unit, '(a)', advance='no', iostat=stat, iomsg=msg, size=size) &
+ & buffer
+ if (stat > 0) exit
+ line = line // buffer(:size)
+ if (stat < 0) then
+ if (is_iostat_eor(stat)) then
+ stat = 0
+ end if
+ exit
+ end if
+ end do
+
+ if (stat /= 0) then
+ if (present(iomsg)) iomsg = trim(msg)
+ end if
+ iostat = stat
+
+end subroutine getline
+
+
+subroutine delete_file(file)
+ character(len=*), intent(in) :: file
+ logical :: exist
+ integer :: unit
+ inquire(file=file, exist=exist)
+ if (exist) then
+ open(file=file, newunit=unit)
+ close(unit, status="delete")
+ end if
+end subroutine delete_file
+
+
end module fpm_filesystem
diff --git a/fpm/src/fpm_model.f90 b/fpm/src/fpm_model.f90
index 1b38d59..a40aef0 100644
--- a/fpm/src/fpm_model.f90
+++ b/fpm/src/fpm_model.f90
@@ -27,6 +27,7 @@
module fpm_model
use iso_fortran_env, only: int64
use fpm_strings, only: string_t
+use fpm_dependency, only: dependency_tree_t
implicit none
private
@@ -187,6 +188,9 @@ type :: fpm_model_t
!> Native libraries to link against
type(string_t), allocatable :: link_libraries(:)
+ !> Project dependencies
+ type(dependency_tree_t) :: deps
+
end type fpm_model_t
end module fpm_model
diff --git a/fpm/test/fpm_test/main.f90 b/fpm/test/fpm_test/main.f90
index 1ba5c6a..bc81dc1 100644
--- a/fpm/test/fpm_test/main.f90
+++ b/fpm/test/fpm_test/main.f90
@@ -7,6 +7,7 @@ program fpm_testing
use test_manifest, only : collect_manifest
use test_source_parsing, only : collect_source_parsing
use test_module_dependencies, only : collect_module_dependencies
+ use test_package_dependencies, only : collect_package_dependencies
use test_backend, only: collect_backend
use test_versioning, only : collect_versioning
implicit none
@@ -22,6 +23,7 @@ program fpm_testing
& new_testsuite("fpm_manifest", collect_manifest), &
& new_testsuite("fpm_source_parsing", collect_source_parsing), &
& new_testsuite("fpm_module_dependencies", collect_module_dependencies), &
+ & new_testsuite("fpm_package_dependencies", collect_package_dependencies), &
& new_testsuite("fpm_test_backend", collect_backend), &
& new_testsuite("fpm_versioning", collect_versioning) &
& ]
diff --git a/fpm/test/fpm_test/test_package_dependencies.f90 b/fpm/test/fpm_test/test_package_dependencies.f90
new file mode 100644
index 0000000..b70ac13
--- /dev/null
+++ b/fpm/test/fpm_test/test_package_dependencies.f90
@@ -0,0 +1,240 @@
+!> Define tests for the `fpm_dependency` module
+module test_package_dependencies
+ use fpm_filesystem, only: get_temp_filename
+ use testsuite, only : new_unittest, unittest_t, error_t, test_failed
+ use fpm_dependency
+ use fpm_manifest
+ use fpm_manifest_dependency
+ use fpm_toml
+ implicit none
+ private
+
+ public :: collect_package_dependencies
+
+ type, extends(dependency_tree_t) :: mock_dependency_tree_t
+ contains
+ procedure :: resolve_dependency => resolve_dependency_once
+ end type mock_dependency_tree_t
+
+
+contains
+
+
+ !> Collect all exported unit tests
+ subroutine collect_package_dependencies(testsuite)
+
+ !> Collection of tests
+ type(unittest_t), allocatable, intent(out) :: testsuite(:)
+
+ testsuite = [ &
+ & new_unittest("cache-load-dump", test_cache_load_dump), &
+ & new_unittest("cache-dump-load", test_cache_dump_load), &
+ & new_unittest("status-after-load", test_status), &
+ & new_unittest("add-dependencies", test_add_dependencies)]
+
+ end subroutine collect_package_dependencies
+
+
+ !> Round trip of the dependency cache from a dependency tree to a TOML document
+ !> to a dependency tree
+ subroutine test_cache_dump_load(error)
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(dependency_tree_t) :: deps
+ type(dependency_config_t) :: dep
+ integer :: unit
+
+ call new_dependency_tree(deps)
+ call resize(deps%dep, 5)
+ deps%ndep = 3
+ dep%name = "dep1"
+ dep%path = "fpm-tmp1-dir"
+ call new_dependency_node(deps%dep(1), dep, proj_dir=dep%path)
+ dep%name = "dep2"
+ dep%path = "fpm-tmp2-dir"
+ call new_dependency_node(deps%dep(2), dep, proj_dir=dep%path)
+ dep%name = "dep3"
+ dep%path = "fpm-tmp3-dir"
+ call new_dependency_node(deps%dep(3), dep, proj_dir=dep%path)
+
+ open(newunit=unit, status='scratch')
+ call deps%dump(unit, error)
+ if (.not.allocated(error)) then
+ rewind(unit)
+
+ call new_dependency_tree(deps)
+ call resize(deps%dep, 2)
+ call deps%load(unit, error)
+ close(unit)
+ end if
+ if (allocated(error)) return
+
+ if (deps%ndep /= 3) then
+ call test_failed(error, "Expected three dependencies in loaded cache")
+ return
+ end if
+
+ end subroutine test_cache_dump_load
+
+
+ !> Round trip of the dependency cache from a TOML data structure to
+ !> a dependency tree to a TOML data structure
+ subroutine test_cache_load_dump(error)
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_table) :: table
+ type(toml_table), pointer :: ptr
+ type(toml_key), allocatable :: list(:)
+ type(dependency_tree_t) :: deps
+
+ table = toml_table()
+ call add_table(table, "dep1", ptr)
+ call set_value(ptr, "version", "1.1.0")
+ call set_value(ptr, "proj-dir", "fpm-tmp1-dir")
+ call add_table(table, "dep2", ptr)
+ call set_value(ptr, "version", "0.55.3")
+ call set_value(ptr, "proj-dir", "fpm-tmp2-dir")
+ call set_value(ptr, "git", "https://github.com/fortran-lang/dep2")
+ call add_table(table, "dep3", ptr)
+ call set_value(ptr, "version", "20.1.15")
+ call set_value(ptr, "proj-dir", "fpm-tmp3-dir")
+ call set_value(ptr, "git", "https://gitlab.com/fortran-lang/dep3")
+ call set_value(ptr, "rev", "c0ffee")
+ call add_table(table, "dep4", ptr)
+ call set_value(ptr, "proj-dir", "fpm-tmp4-dir")
+
+ call new_dependency_tree(deps)
+ call deps%load(table, error)
+ if (allocated(error)) return
+
+ if (deps%ndep /= 4) then
+ call test_failed(error, "Expected four dependencies in loaded cache")
+ return
+ end if
+
+ call table%destroy
+ table = toml_table()
+
+ call deps%dump(table, error)
+ if (allocated(error)) return
+
+ call table%get_keys(list)
+
+ if (size(list) /= 4) then
+ call test_failed(error, "Expected four dependencies in dumped cache")
+ return
+ end if
+
+ end subroutine test_cache_load_dump
+
+
+ subroutine test_status(error)
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_table) :: table
+ type(toml_table), pointer :: ptr
+ type(toml_key), allocatable :: list(:)
+ type(dependency_tree_t) :: deps
+
+ table = toml_table()
+ call add_table(table, "dep1", ptr)
+ call set_value(ptr, "version", "1.1.0")
+ call set_value(ptr, "proj-dir", "fpm-tmp1-dir")
+ call add_table(table, "dep2", ptr)
+ call set_value(ptr, "version", "0.55.3")
+ call set_value(ptr, "proj-dir", "fpm-tmp2-dir")
+ call set_value(ptr, "git", "https://github.com/fortran-lang/dep2")
+
+ call new_dependency_tree(deps)
+ call deps%load(table, error)
+ if (allocated(error)) return
+
+ if (deps%finished()) then
+ call test_failed(error, "Newly initialized dependency tree cannot be reolved")
+ return
+ end if
+
+ end subroutine test_status
+
+
+ subroutine test_add_dependencies(error)
+
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(toml_table) :: table
+ type(toml_table), pointer :: child, ptr
+ type(toml_key), allocatable :: list(:)
+ type(mock_dependency_tree_t) :: deps
+ type(dependency_config_t), allocatable :: nodes(:)
+
+ table = toml_table()
+ call add_table(table, "sub1", ptr)
+ call set_value(ptr, "path", "external")
+ call add_table(table, "lin2", ptr)
+ call set_value(ptr, "git", "https://github.com/fortran-lang/lin2")
+ call add_table(table, "pkg3", ptr)
+ call set_value(ptr, "git", "https://gitlab.com/fortran-lang/pkg3")
+ call set_value(ptr, "rev", "c0ffee")
+ call add_table(table, "proj4", ptr)
+ call set_value(ptr, "path", "vendor")
+
+ call new_dependencies(nodes, table, error)
+ if (allocated(error)) return
+
+ call new_dependency_tree(deps%dependency_tree_t)
+ call deps%add(nodes, error)
+ if (allocated(error)) return
+
+ if (deps%finished()) then
+ call test_failed(error, "Newly added nodes cannot be already resolved")
+ return
+ end if
+
+ if (deps%ndep /= 4) then
+ call test_failed(error, "Expected for dependencies in tree")
+ return
+ end if
+
+ call deps%resolve(".", error)
+ if (allocated(error)) return
+
+ if (.not.deps%finished()) then
+ call test_failed(error, "Mocked dependency tree must resolve in one step")
+ return
+ end if
+
+ end subroutine test_add_dependencies
+
+
+ !> Resolve a single dependency node
+ subroutine resolve_dependency_once(self, dependency, root, error)
+ !> Mock instance of the dependency tree
+ class(mock_dependency_tree_t), intent(inout) :: self
+ !> Dependency configuration to add
+ type(dependency_node_t), intent(inout) :: dependency
+ !> Current installation prefix
+ character(len=*), intent(in) :: root
+ !> Error handling
+ type(error_t), allocatable, intent(out) :: error
+
+ type(package_config_t) :: package
+ character(len=:), allocatable :: manifest, proj_dir, revision
+ logical :: fetch
+
+ if (dependency%done) then
+ call test_failed(error, "Should only visit this node once")
+ return
+ end if
+ dependency%done = .true.
+
+ end subroutine resolve_dependency_once
+
+
+end module test_package_dependencies
diff --git a/fpm/test/help_test/help_test.f90 b/fpm/test/help_test/help_test.f90
index 390b274..eb452b1 100644
--- a/fpm/test/help_test/help_test.f90
+++ b/fpm/test/help_test/help_test.f90
@@ -22,6 +22,7 @@ character(len=*),parameter :: cmds(*) = [character(len=80) :: &
'fpm run -- --version ',& ! verify fpm version being used
'fpm run -- --help > fpm_scratch_help.txt',&
'fpm run -- help new >> fpm_scratch_help.txt',&
+'fpm run -- help update >> fpm_scratch_help.txt',&
'fpm run -- build --help >> fpm_scratch_help.txt',&
'fpm run -- help run >> fpm_scratch_help.txt',&
'fpm run -- help test >> fpm_scratch_help.txt',&
@@ -33,6 +34,7 @@ character(len=*),parameter :: cmds(*) = [character(len=80) :: &
'fpm run --release -- --version ',& ! verify fpm version being used
'fpm run --release -- --help > fpm_scratch_help3.txt',&
'fpm run --release -- help new >> fpm_scratch_help3.txt',&
+'fpm run --release -- help update >> fpm_scratch_help3.txt',&
'fpm run --release -- build --help >> fpm_scratch_help3.txt',&
'fpm run --release -- help run >> fpm_scratch_help3.txt',&
'fpm run --release -- help test >> fpm_scratch_help3.txt',&
@@ -46,7 +48,7 @@ character(len=*),parameter :: cmds(*) = [character(len=80) :: &
!'fpm run >> fpm_scratch_help.txt',&
!'fpm run -- --list >> fpm_scratch_help.txt',&
!'fpm run -- list --list >> fpm_scratch_help.txt',&
-character(len=*),parameter :: names(*)=[character(len=10) :: 'fpm','new','build','run','test','runner','list','help']
+character(len=*),parameter :: names(*)=[character(len=10) :: 'fpm','new','update','build','run','test','runner','list','help']
character(len=:),allocatable :: add
write(*,'(g0:,1x)')'<INFO>TEST help SUBCOMMAND STARTED'