From 63a565fab4300e43fbd2d06b23d3c1f2a4e1976e Mon Sep 17 00:00:00 2001 From: Sebastian Ehlert <28669218+awvwgk@users.noreply.github.com> Date: Thu, 10 Dec 2020 17:58:41 +0100 Subject: Dependency handling (#266) - create a dependency tree data type with serde functionality - allow translation from and to TOML of dependency tree - test round trips for dependency cache - implement iterative fetching of each dependency level - update dependencies in fpm-update (rudimentary implementation only) - allow removing the cache from the command line - use dependency tree for fpm-build model - add fpm-update to fpm-help/fpm-list output --- fpm/app/main.f90 | 4 + fpm/fpm.toml | 2 +- fpm/src/fpm.f90 | 153 +---- fpm/src/fpm/cmd/update.f90 | 68 ++ fpm/src/fpm/dependency.f90 | 811 ++++++++++++++++++++++++ fpm/src/fpm/git.f90 | 61 +- fpm/src/fpm/manifest.f90 | 22 +- fpm/src/fpm/toml.f90 | 4 +- fpm/src/fpm/versioning.f90 | 20 +- fpm/src/fpm_command_line.f90 | 52 +- fpm/src/fpm_filesystem.f90 | 56 +- fpm/src/fpm_model.f90 | 4 + fpm/test/fpm_test/main.f90 | 2 + fpm/test/fpm_test/test_package_dependencies.f90 | 240 +++++++ fpm/test/help_test/help_test.f90 | 4 +- 15 files changed, 1354 insertions(+), 149 deletions(-) create mode 100644 fpm/src/fpm/cmd/update.f90 create mode 100644 fpm/src/fpm/dependency.f90 create mode 100644 fpm/test/fpm_test/test_package_dependencies.f90 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(*,*)'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)')'TEST help SUBCOMMAND STARTED' -- cgit v1.2.3