dependency.f90 Source File


Source Code

!> # 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, os_is_unix
  use fpm_error, only: error_t, fatal_error
  use fpm_filesystem, only: exists, join_path, mkdir, canon_path, windows_path, list_files, is_dir, basename, &
                            os_delete_dir, get_temp_filename
  use fpm_git, only: git_target_revision, git_target_default, git_revision, serializable_t
  use fpm_manifest, only: package_config_t, dependency_config_t, get_package_data
  use fpm_manifest_dependency, only: manifest_has_changed, dependency_destroy
  use fpm_manifest_preprocess, only: operator(==)
  use fpm_strings, only: string_t, operator(.in.)
  use fpm_toml, only: toml_table, toml_key, toml_error, toml_serialize, &
                      get_value, set_value, add_table, toml_load, toml_stat, set_string
  use fpm_versioning, only: version_t, new_version
  use fpm_settings, only: fpm_global_settings, get_global_settings, official_registry_base_url
  use fpm_downloader, only: downloader_t
  use jonquil, only: json_object
  use fpm_strings, only: str
  implicit none
  private

  public :: dependency_tree_t, new_dependency_tree, dependency_node_t, new_dependency_node, resize, &
            & check_and_read_pkg_data, destroy_dependency_node

  !> 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.
    !> Dependency was loaded from a cache
    logical :: cached = .false.
  contains
    !> Update dependency from project manifest.
    procedure :: register
    !> Get dependency from the registry.
    procedure :: get_from_registry
    procedure, private :: get_from_local_registry
    !> Print information on this instance
    procedure :: info

    !> Serialization interface
    procedure :: serializable_is_same => dependency_node_is_same
    procedure :: dump_to_toml => node_dump_to_toml
    procedure :: load_from_toml => node_load_from_toml

  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, extends(serializable_t) :: 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, add_dependency_node
    !> 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
    !> Add a single dependency node to the dependency tree
    procedure, private :: add_dependency_node
    !> Resolve dependencies
    generic :: resolve => resolve_dependencies, resolve_dependency
    !> Resolve dependencies
    procedure, private :: resolve_dependencies
    !> Resolve dependency
    procedure, private :: resolve_dependency
    !> True if entity can be found
    generic :: has => has_dependency
    !> True if dependency is part of the tree
    procedure, private :: has_dependency
    !> Find a dependency in the tree
    generic :: find => find_name
    !> Find a dependency by its name
    procedure, private :: find_name
    !> Depedendncy resolution finished
    procedure :: finished
    !> Reading of dependency tree
    generic :: load_cache => load_cache_from_file, load_cache_from_unit, load_cache_from_toml
    !> Read dependency tree from file
    procedure, private :: load_cache_from_file
    !> Read dependency tree from formatted unit
    procedure, private :: load_cache_from_unit
    !> Read dependency tree from TOML data structure
    procedure, private :: load_cache_from_toml
    !> Writing of dependency tree
    generic :: dump_cache => dump_cache_to_file, dump_cache_to_unit, dump_cache_to_toml
    !> Write dependency tree to file
    procedure, private :: dump_cache_to_file
    !> Write dependency tree to formatted unit
    procedure, private :: dump_cache_to_unit
    !> Write dependency tree to TOML data structure
    procedure, private :: dump_cache_to_toml
    !> Update dependency tree
    generic :: update => update_dependency, update_tree
    !> Update a list of dependencies
    procedure, private :: update_dependency
    !> Update all dependencies in the tree
    procedure, private :: update_tree

    !> Serialization interface
    procedure :: serializable_is_same => dependency_tree_is_same
    procedure :: dump_to_toml   => tree_dump_to_toml
    procedure :: load_from_toml => tree_load_from_toml

  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)) self%verbosity = verbosity

    if (present(cache)) self%cache = cache

  end subroutine new_dependency_tree

  !> Create a new dependency node from a configuration
  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

  !> Write information on instance
  subroutine info(self, unit, verbosity)

    !> Instance of the dependency configuration
    class(dependency_node_t), intent(in) :: self

    !> Unit for IO
    integer, intent(in) :: unit

    !> Verbosity of the printout
    integer, intent(in), optional :: verbosity

    integer :: pr
    character(len=*), parameter :: fmt = '("#", 1x, a, t30, a)'

    if (present(verbosity)) then
      pr = verbosity
    else
      pr = 1
    end if

    !> Call base object info
    call self%dependency_config_t%info(unit, pr)

    if (allocated(self%version)) then
      write (unit, fmt) "- version", self%version%s()
    end if

    if (allocated(self%proj_dir)) then
      write (unit, fmt) "- dir", self%proj_dir
    end if

    if (allocated(self%revision)) then
      write (unit, fmt) "- revision", self%revision
    end if

    write (unit, fmt) "- done", merge('YES', 'NO ', self%done)
    write (unit, fmt) "- update", merge('YES', 'NO ', self%update)

  end subroutine info

  !> 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
    type(dependency_tree_t) :: cached
    character(len=*), parameter :: root = '.'
    integer :: id

    if (.not. exists(self%dep_dir)) then
      call mkdir(self%dep_dir)
    end if

    ! 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

    ! After resolving all dependencies, check if we have cached ones to avoid updates
    if (allocated(self%cache)) then
      call new_dependency_tree(cached, verbosity=self%verbosity,cache=self%cache)
      call cached%load_cache(self%cache, error)
      if (allocated(error)) return

      ! Skip root node
      do id = 2, cached%ndep
        cached%dep(id)%cached = .true.
        call self%add(cached%dep(id), error)
        if (allocated(error)) return
      end do
    end if

    ! 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_cache(self%cache, error)
      if (allocated(error)) return
    end if

  end subroutine add_project

  !> Add a project and its dependencies to the dependency tree
  recursive subroutine add_project_dependencies(self, package, root, main, error)
    !> Instance of the dependency tree
    class(dependency_tree_t), intent(inout) :: self
    !> Project configuration to add
    type(package_config_t), intent(in) :: package
    !> Current project root directory
    character(len=*), intent(in) :: root
    !> Is the main project
    logical, intent(in) :: main
    !> Error handling
    type(error_t), allocatable, intent(out) :: error

    integer :: ii

    if (allocated(package%dependency)) then
      call self%add(package%dependency, error)
      if (allocated(error)) return
    end if

    if (main) then
      if (allocated(package%dev_dependency)) then
        call self%add(package%dev_dependency, error)
        if (allocated(error)) return
      end if

      if (allocated(package%executable)) then
        do ii = 1, size(package%executable)
          if (allocated(package%executable(ii)%dependency)) then
            call self%add(package%executable(ii)%dependency, error)
            if (allocated(error)) exit
          end if
        end do
        if (allocated(error)) return
      end if

      if (allocated(package%example)) then
        do ii = 1, size(package%example)
          if (allocated(package%example(ii)%dependency)) then
            call self%add(package%example(ii)%dependency, error)
            if (allocated(error)) exit
          end if
        end do
        if (allocated(error)) return
      end if

      if (allocated(package%test)) then
        do ii = 1, size(package%test)
          if (allocated(package%test(ii)%dependency)) then
            call self%add(package%test(ii)%dependency, error)
            if (allocated(error)) exit
          end if
        end do
        if (allocated(error)) return
      end if
    end if

    !> Ensure allocation fits
    call resize(self%dep,self%ndep)

  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

    !> Ensure allocation fits ndep
    call resize(self%dep,self%ndep)

  end subroutine add_dependencies

  !> Add a single dependency node to the dependency tree
  !> Dependency nodes contain additional information (version, git, revision)
  subroutine add_dependency_node(self, dependency, error)
    !> Instance of the dependency tree
    class(dependency_tree_t), intent(inout) :: self
    !> Dependency configuration to add
    type(dependency_node_t), intent(in) :: dependency
    !> Error handling
    type(error_t), allocatable, intent(out) :: error

    integer :: id

    if (self%has_dependency(dependency)) then
      ! A dependency with this same name is already in the dependency tree.
      ! Check if it needs to be updated
      id = self%find(dependency%name)

      ! If this dependency was in the cache, and we're now requesting a different version
      ! in the manifest, ensure it is marked for update. Otherwise, if we're just querying
      ! the same dependency from a lower branch of the dependency tree, the existing one from
      ! the manifest has priority
      if (dependency%cached) then
        if (dependency_has_changed(dependency, self%dep(id), self%verbosity, self%unit)) then
          if (self%verbosity > 0) write (self%unit, out_fmt) "Dependency change detected:", dependency%name
          self%dep(id)%update = .true.
        else
          ! Store the cached one
          self%dep(id) = dependency
          self%dep(id)%update = .false.
        end if
      end if
    else

      !> Safety: reallocate if necessary
      if (size(self%dep)==self%ndep) call resize(self%dep,self%ndep+1)

      ! New dependency: add from scratch
      self%ndep = self%ndep + 1
      self%dep(self%ndep) = dependency
      self%dep(self%ndep)%update = .false.
    end if

  end subroutine add_dependency_node

  !> Add a single dependency to the dependency tree
  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

    type(dependency_node_t) :: node

    call new_dependency_node(node, dependency)
    call add_dependency_node(self, node, error)

  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
    character(len=:), allocatable :: proj_dir, 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 > 0) write (self%unit, out_fmt) "Update:", dep%name
        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

  !> Update whole dependency tree
  subroutine update_tree(self, error)
    !> Instance of the dependency tree
    class(dependency_tree_t), intent(inout) :: self
    !> Error handling
    type(error_t), allocatable, intent(out) :: error

    integer :: i

    ! Update dependencies where needed
    do i = 1, self%ndep
      call self%update(self%dep(i)%name, error)
      if (allocated(error)) return
    end do

  end subroutine update_tree

  !> 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

    type(fpm_global_settings) :: global_settings
    integer :: ii

    call get_global_settings(global_settings, error)
    if (allocated(error)) return

    do ii = 1, self%ndep
      call self%resolve(self%dep(ii), global_settings, 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, global_settings, 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
    !> Global configuration settings.
    type(fpm_global_settings), intent(in) :: global_settings
    !> 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
    else
      call dependency%get_from_registry(proj_dir, global_settings, error)
      if (allocated(error)) return
    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", dependency%version%s(), &
        "at", dependency%proj_dir
    end if

    call self%add(package, proj_dir, .false., error)
    if (allocated(error)) return

  end subroutine resolve_dependency

  !> Get a dependency from the registry. Whether the dependency is fetched
  !> from a local, a custom remote or the official registry is determined
  !> by the global configuration settings.
  subroutine get_from_registry(self, target_dir, global_settings, error, downloader_)

    !> Instance of the dependency configuration.
    class(dependency_node_t), intent(in) :: self

    !> The target directory of the dependency.
    character(:), allocatable, intent(out) :: target_dir

    !> Global configuration settings.
    type(fpm_global_settings), intent(in) :: global_settings

    !> Error handling.
    type(error_t), allocatable, intent(out) :: error

    !> Downloader instance.
    class(downloader_t), optional, intent(in) :: downloader_

    character(:), allocatable :: cache_path, target_url, tmp_file
    type(version_t) :: version
    integer :: stat, unit
    type(json_object) :: json
    class(downloader_t), allocatable :: downloader

    if (present(downloader_)) then
      downloader = downloader_
    else
      allocate (downloader)
    end if

    ! Use local registry if it was specified in the global config file.
    if (allocated(global_settings%registry_settings%path)) then
      call self%get_from_local_registry(target_dir, global_settings%registry_settings%path, error); return
    end if

    ! Include namespace and package name in the cache path.
    cache_path = join_path(global_settings%registry_settings%cache_path, self%namespace, self%name)

    ! Check cache before downloading from the remote registry if a specific version was requested. When no specific
    ! version was requested, do network request first to check which is the newest version.
    if (allocated(self%requested_version)) then
      if (exists(join_path(cache_path, self%requested_version%s(), 'fpm.toml'))) then
        print *, "Using cached version of '", join_path(self%namespace, self%name, self%requested_version%s()), "'."
        target_dir = join_path(cache_path, self%requested_version%s()); return
      end if
    end if

    tmp_file = get_temp_filename()
    open (newunit=unit, file=tmp_file, action='readwrite', iostat=stat)
    if (stat /= 0) then
      call fatal_error(error, "Error creating temporary file for downloading package '"//self%name//"'."); return
    end if

    ! Include namespace and package name in the target url and download package data.
    target_url = global_settings%registry_settings%url//'packages/'//self%namespace//'/'//self%name
    call downloader%get_pkg_data(target_url, self%requested_version, tmp_file, json, error)
    close (unit, status='delete')
    if (allocated(error)) return

    ! Verify package data and read relevant information.
    call check_and_read_pkg_data(json, self, target_url, version, error)
    if (allocated(error)) return

    ! Open new tmp file for downloading the actual package.
    open (newunit=unit, file=tmp_file, action='readwrite', iostat=stat)
    if (stat /= 0) then
      call fatal_error(error, "Error creating temporary file for downloading package '"//self%name//"'."); return
    end if

    ! Include version number in the cache path. If no cached version exists, download it.
    cache_path = join_path(cache_path, version%s())
    if (.not. exists(join_path(cache_path, 'fpm.toml'))) then
      if (is_dir(cache_path)) call os_delete_dir(os_is_unix(), cache_path)
      call mkdir(cache_path)

      call downloader%get_file(target_url, tmp_file, error)
      if (allocated(error)) then
        close (unit, status='delete'); return
      end if

      ! Unpack the downloaded package to the final location.
      call downloader%unpack(tmp_file, cache_path, error)
      close (unit, status='delete')
      if (allocated(error)) return
    end if

    target_dir = cache_path

  end subroutine get_from_registry

  subroutine check_and_read_pkg_data(json, node, download_url, version, error)
    type(json_object), intent(inout) :: json
    class(dependency_node_t), intent(in) :: node
    character(:), allocatable, intent(out) :: download_url
    type(version_t), intent(out) :: version
    type(error_t), allocatable, intent(out) :: error

    integer :: code, stat
    type(json_object), pointer :: p, q
    character(:), allocatable :: version_key, version_str, error_message, namespace, name

    namespace = ""
    name = "UNNAMED_NODE"
    if (allocated(node%namespace)) namespace = node%namespace
    if (allocated(node%name)) name = node%name

    if (.not. json%has_key('code')) then
      call fatal_error(error, "Failed to download '"//join_path(namespace, name)//"': No status code."); return
    end if

    call get_value(json, 'code', code, stat=stat)
    if (stat /= 0) then
      call fatal_error(error, "Failed to download '"//join_path(namespace, name)//"': "// &
      & "Failed to read status code."); return
    end if

    if (code /= 200) then
      if (.not. json%has_key('message')) then
        call fatal_error(error, "Failed to download '"//join_path(namespace, name)//"': No error message."); return
      end if

      call get_value(json, 'message', error_message, stat=stat)
      if (stat /= 0) then
        call fatal_error(error, "Failed to download '"//join_path(namespace, name)//"': "// &
        & "Failed to read error message."); return
      end if

      call fatal_error(error, "Failed to download '"//join_path(namespace, name)//"'. Status code: '"// &
      & str(code)//"'. Error message: '"//error_message//"'."); return
    end if

    if (.not. json%has_key('data')) then
      call fatal_error(error, "Failed to download '"//join_path(namespace, name)//"': No data."); return
    end if

    call get_value(json, 'data', p, stat=stat)
    if (stat /= 0) then
      call fatal_error(error, "Failed to read package data for '"//join_path(namespace, name)//"'."); return
    end if

    if (allocated(node%requested_version)) then
      version_key = 'version_data'
    else
      version_key = 'latest_version_data'
    end if

    if (.not. p%has_key(version_key)) then
      call fatal_error(error, "Failed to download '"//join_path(namespace, name)//"': No version data."); return
    end if

    call get_value(p, version_key, q, stat=stat)
    if (stat /= 0) then
      call fatal_error(error, "Failed to retrieve version data for '"//join_path(namespace, name)//"'."); return
    end if

    if (.not. q%has_key('download_url')) then
      call fatal_error(error, "Failed to download '"//join_path(namespace, name)//"': No download url."); return
    end if

    call get_value(q, 'download_url', download_url, stat=stat)
    if (stat /= 0) then
      call fatal_error(error, "Failed to read download url for '"//join_path(namespace, name)//"'."); return
    end if

    download_url = official_registry_base_url//download_url

    if (.not. q%has_key('version')) then
      call fatal_error(error, "Failed to download '"//join_path(namespace, name)//"': No version found."); return
    end if

    call get_value(q, 'version', version_str, stat=stat)
    if (stat /= 0) then
      call fatal_error(error, "Failed to read version data for '"//join_path(namespace, name)//"'."); return
    end if

    call new_version(version, version_str, error)
    if (allocated(error)) then
      call fatal_error(error, "'"//version_str//"' is not a valid version for '"// &
      & join_path(namespace, name)//"'."); return
    end if
  end subroutine

  !> Get the dependency from a local registry.
  subroutine get_from_local_registry(self, target_dir, registry_path, error)

    !> Instance of the dependency configuration.
    class(dependency_node_t), intent(in) :: self

    !> The target directory to download the dependency to.
    character(:), allocatable, intent(out) :: target_dir

    !> The path to the local registry.
    character(*), intent(in) :: registry_path

    !> Error handling.
    type(error_t), allocatable, intent(out) :: error

    character(:), allocatable :: path_to_name
    type(string_t), allocatable :: files(:)
    type(version_t), allocatable :: versions(:)
    type(version_t) :: version
    integer :: i

    path_to_name = join_path(registry_path, self%namespace, self%name)

    if (.not. exists(path_to_name)) then
      call fatal_error(error, "Dependency resolution of '"//self%name// &
      & "': Directory '"//path_to_name//"' doesn't exist."); return
    end if

    call list_files(path_to_name, files)
    if (size(files) == 0) then
      call fatal_error(error, "No versions of '"//self%name//"' found in '"//path_to_name//"'."); return
    end if

    ! Version requested, find it in the cache.
    if (allocated(self%requested_version)) then
      do i = 1, size(files)
        ! Identify directory that matches the version number.
        if (files(i)%s == join_path(path_to_name, self%requested_version%s()) .and. is_dir(files(i)%s)) then
          if (.not. exists(join_path(files(i)%s, 'fpm.toml'))) then
            call fatal_error(error, "'"//files(i)%s//"' is missing an 'fpm.toml' file."); return
          end if
          target_dir = files(i)%s; return
        end if
      end do
      call fatal_error(error, "Version '"//self%requested_version%s()//"' not found in '"//path_to_name//"'")
      return
    end if

    ! No specific version requested, therefore collect available versions.
    allocate (versions(0))
    do i = 1, size(files)
      if (is_dir(files(i)%s)) then
        call new_version(version, basename(files(i)%s), error)
        if (allocated(error)) return
        versions = [versions, version]
      end if
    end do

    if (size(versions) == 0) then
      call fatal_error(error, "No versions found in '"//path_to_name//"'"); return
    end if

    ! Find the latest version.
    version = versions(1)
    do i = 1, size(versions)
      if (versions(i) > version) version = versions(i)
    end do

    path_to_name = join_path(path_to_name, version%s())

    if (.not. exists(join_path(path_to_name, 'fpm.toml'))) then
      call fatal_error(error, "'"//path_to_name//"' is missing an 'fpm.toml' file."); return
    end if

    target_dir = path_to_name
  end subroutine get_from_local_registry

  !> True if dependency is part of the tree
  pure logical function has_dependency(self, dependency)
    !> Instance of the dependency tree
    class(dependency_tree_t), intent(in) :: self
    !> Dependency configuration to check
    class(dependency_node_t), intent(in) :: dependency

    has_dependency = self%find(dependency%name) /= 0

  end function has_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

    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

    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
        ! Change in revision ID was checked already. Only update if ALL git information is missing
        update = .not. allocated(self%git%url)
      end if
    end if

    if (update) self%update = update
    self%done = .true.

  end subroutine register

  !> Read dependency tree from file
  subroutine load_cache_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_cache(unit, error)
    close (unit)
  end subroutine load_cache_from_file

  !> Read dependency tree from file
  subroutine load_cache_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_load(table, unit, error=parse_error)

    if (allocated(parse_error)) then
      allocate (error)
      call move_alloc(parse_error%message, error%message)
      return
    end if

    call self%load_cache(table, error)
    if (allocated(error)) return

  end subroutine load_cache_from_unit

  !> Read dependency tree from TOML data structure
  subroutine load_cache_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 :: is_unix
    character(len=:), allocatable :: version, url, obj, rev, proj_dir
    type(toml_key), allocatable :: list(:)
    type(toml_table), pointer :: ptr

    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

    is_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 (is_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(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_cache_from_toml

  !> Write dependency tree to file
  subroutine dump_cache_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_cache(unit, error)
    close (unit)
    if (allocated(error)) return

  end subroutine dump_cache_to_file

  !> Write dependency tree to file
  subroutine dump_cache_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

    table = toml_table()
    call self%dump_cache(table, error)

    write (unit, '(a)') toml_serialize(table)

  end subroutine dump_cache_to_unit

  !> Write dependency tree to TOML datastructure
  subroutine dump_cache_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", dep%version%s())
        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_cache_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

  !> Check if a dependency node has changed
  logical function dependency_has_changed(cached, manifest, verbosity, iunit) result(has_changed)
    !> Two instances of the same dependency to be compared
    type(dependency_node_t), intent(in) :: cached, manifest

    !> Log verbosity
    integer, intent(in) :: verbosity, iunit

    integer :: ip

    has_changed = .true.

    !> All the following entities must be equal for the dependency to not have changed
    if (manifest_has_changed(cached=cached, manifest=manifest, verbosity=verbosity, iunit=iunit)) return

    !> For now, only perform the following checks if both are available. A dependency in cache.toml
    !> will always have this metadata; a dependency from fpm.toml which has not been fetched yet
    !> may not have it
    if (allocated(cached%version) .and. allocated(manifest%version)) then
      if (cached%version /= manifest%version) then
        if (verbosity > 1) write (iunit, out_fmt) "VERSION has changed: "//cached%version%s()//" vs. "//manifest%version%s()
        return
      end if
    else
      if (verbosity > 1) write (iunit, out_fmt) "VERSION has changed presence "
    end if
    if (allocated(cached%revision) .and. allocated(manifest%revision)) then
      if (cached%revision /= manifest%revision) then
        if (verbosity > 1) write (iunit, out_fmt) "REVISION has changed: "//cached%revision//" vs. "//manifest%revision
        return
      end if
    else
      if (verbosity > 1) write (iunit, out_fmt) "REVISION has changed presence "
    end if
    if (allocated(cached%proj_dir) .and. allocated(manifest%proj_dir)) then
      if (cached%proj_dir /= manifest%proj_dir) then
        if (verbosity > 1) write (iunit, out_fmt) "PROJECT DIR has changed: "//cached%proj_dir//" vs. "//manifest%proj_dir
        return
      end if
    else
      if (verbosity > 1) write (iunit, out_fmt) "PROJECT DIR has changed presence "
    end if
    if (allocated(cached%preprocess) .eqv. allocated(manifest%preprocess)) then
      if (allocated(cached%preprocess)) then
          if (size(cached%preprocess) /= size(manifest%preprocess)) then
            if (verbosity > 1) write (iunit, out_fmt) "PREPROCESS has changed size"
            return
          end if
          do ip=1,size(cached%preprocess)
             if (.not.(cached%preprocess(ip) == manifest%preprocess(ip))) then
                if (verbosity > 1) write (iunit, out_fmt) "PREPROCESS config has changed"
                return
             end if
          end do
      endif
    else
      if (verbosity > 1) write (iunit, out_fmt) "PREPROCESS has changed presence "
      return
    end if

    !> All checks passed: the two dependencies have no differences
    has_changed = .false.

  end function dependency_has_changed

  !> Check that two dependency nodes are equal
  logical function dependency_node_is_same(this,that)
      class(dependency_node_t), intent(in) :: this
      class(serializable_t), intent(in) :: that

      dependency_node_is_same = .false.

      select type (other=>that)
         type is (dependency_node_t)

            ! Base class must match
            if (.not.(this%dependency_config_t==other%dependency_config_t)) return

            ! Extension must match
            if (.not.(this%done  .eqv.other%done)) return
            if (.not.(this%update.eqv.other%update)) return
            if (.not.(this%cached.eqv.other%cached)) return
            if (.not.(this%proj_dir==other%proj_dir)) return
            if (.not.(this%revision==other%revision)) return

            if (.not.(allocated(this%version).eqv.allocated(other%version))) return
               if (allocated(this%version)) then
              if (.not.(this%version==other%version)) return
            endif

         class default
            ! Not the same type
            return
      end select

      !> All checks passed!
      dependency_node_is_same = .true.

  end function dependency_node_is_same

    !> Dump dependency to toml table
    subroutine node_dump_to_toml(self, table, error)

        !> Instance of the serializable object
        class(dependency_node_t), intent(inout) :: self

        !> Data structure
        type(toml_table), intent(inout) :: table

        !> Error handling
        type(error_t), allocatable, intent(out) :: error

        integer :: ierr

        ! Dump parent class
        call self%dependency_config_t%dump_to_toml(table, error)
        if (allocated(error)) return

        if (allocated(self%version)) then
            call set_string(table, "version", self%version%s(), error,'dependency_node_t')
            if (allocated(error)) return
        endif
        call set_string(table, "proj-dir", self%proj_dir, error, 'dependency_node_t')
        if (allocated(error)) return
        call set_string(table, "revision", self%revision, error, 'dependency_node_t')
        if (allocated(error)) return
        call set_value(table, "done", self%done, error, 'dependency_node_t')
        if (allocated(error)) return
        call set_value(table, "update", self%update, error, 'dependency_node_t')
        if (allocated(error)) return
        call set_value(table, "cached", self%cached, error, 'dependency_node_t')
        if (allocated(error)) return

    end subroutine node_dump_to_toml

    !> Read dependency from toml table (no checks made at this stage)
    subroutine node_load_from_toml(self, table, error)

        !> Instance of the serializable object
        class(dependency_node_t), intent(inout) :: self

        !> Data structure
        type(toml_table), intent(inout) :: table

        !> Error handling
        type(error_t), allocatable, intent(out) :: error

        !> Local variables
        character(len=:), allocatable :: version
        integer :: ierr

        call destroy_dependency_node(self)

        ! Load parent class
        call self%dependency_config_t%load_from_toml(table, error)
        if (allocated(error)) return

        call get_value(table, "done", self%done, error, 'dependency_node_t')
        if (allocated(error)) return
        call get_value(table, "update", self%update, error, 'dependency_node_t')
        if (allocated(error)) return
        call get_value(table, "cached", self%cached, error, 'dependency_node_t')
        if (allocated(error)) return

        call get_value(table, "proj-dir", self%proj_dir)
        call get_value(table, "revision", self%revision)

        call get_value(table, "version", version)
        if (allocated(version)) then
            allocate(self%version)
            call new_version(self%version, version, error)
            if (allocated(error)) then
                error%message = 'dependency_node_t: version error from TOML table - '//error%message
                return
            endif
        end if

    end subroutine node_load_from_toml

    !> Destructor
    elemental subroutine destroy_dependency_node(self)

        class(dependency_node_t), intent(inout) :: self

        integer :: ierr

        call dependency_destroy(self)

        deallocate(self%version,stat=ierr)
        deallocate(self%proj_dir,stat=ierr)
        deallocate(self%revision,stat=ierr)
        self%done = .false.
        self%update = .false.
        self%cached = .false.

    end subroutine destroy_dependency_node

  !> Check that two dependency trees are equal
  logical function dependency_tree_is_same(this,that)
    class(dependency_tree_t), intent(in) :: this
    class(serializable_t), intent(in) :: that

    integer :: ii

    dependency_tree_is_same = .false.

    select type (other=>that)
       type is (dependency_tree_t)

          if (.not.(this%unit==other%unit)) return
          if (.not.(this%verbosity==other%verbosity)) return
          if (.not.(this%dep_dir==other%dep_dir)) return
          if (.not.(this%ndep==other%ndep)) return
          if (.not.(allocated(this%dep).eqv.allocated(other%dep))) return
          if (allocated(this%dep)) then
             if (.not.(size(this%dep)==size(other%dep))) return
             do ii = 1, size(this%dep)
                if (.not.(this%dep(ii)==other%dep(ii))) return
             end do
          endif
          if (.not.(this%cache==other%cache)) return

       class default
          ! Not the same type
          return
    end select

    !> All checks passed!
    dependency_tree_is_same = .true.

  end function dependency_tree_is_same

    !> Dump dependency to toml table
    subroutine tree_dump_to_toml(self, table, error)

        !> Instance of the serializable object
        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 :: ierr, ii
        type(toml_table), pointer :: ptr_deps,ptr
        character(27) :: unnamed

        call set_value(table, "unit", self%unit, error, 'dependency_tree_t')
        if (allocated(error)) return
        call set_value(table, "verbosity", self%verbosity, error, 'dependency_tree_t')
        if (allocated(error)) return
        call set_string(table, "dep-dir", self%dep_dir, error, 'dependency_tree_t')
        if (allocated(error)) return
        call set_string(table, "cache", self%cache, error, 'dependency_tree_t')
        if (allocated(error)) return
        call set_value(table, "ndep", self%ndep, error, 'dependency_tree_t')
        if (allocated(error)) return

        if (allocated(self%dep)) then

           ! Create dependency table
           call add_table(table, "dependencies", ptr_deps)
           if (.not. associated(ptr_deps)) then
              call fatal_error(error, "dependency_tree_t cannot create dependency table ")
              return
           end if

           do ii = 1, size(self%dep)
              associate (dep => self%dep(ii))

                 !> Because dependencies are named, fallback if this has no name
                 !> So, serialization will work regardless of size(self%dep) == self%ndep
                 if (len_trim(dep%name)==0) then
                    write(unnamed,1) ii
                    call add_table(ptr_deps, trim(unnamed), ptr)
                 else
                    call add_table(ptr_deps, dep%name, ptr)
                 end if
                 if (.not. associated(ptr)) then
                    call fatal_error(error, "dependency_tree_t cannot create entry for dependency "//dep%name)
                    return
                 end if
                 call dep%dump_to_toml(ptr, error)
                 if (allocated(error)) return
              end associate
           end do

        endif

        1 format('UNNAMED_DEPENDENCY_',i0)

    end subroutine tree_dump_to_toml

    !> Read dependency from toml table (no checks made at this stage)
    subroutine tree_load_from_toml(self, table, error)

        !> Instance of the serializable object
        class(dependency_tree_t), intent(inout) :: self

        !> Data structure
        type(toml_table), intent(inout) :: table

        !> Error handling
        type(error_t), allocatable, intent(out) :: error

        !> Local variables
        type(toml_key), allocatable :: keys(:),dep_keys(:)
        type(toml_table), pointer :: ptr_deps,ptr
        integer :: ii, jj, ierr

        call table%get_keys(keys)

        call get_value(table, "unit", self%unit, error, 'dependency_tree_t')
        if (allocated(error)) return
        call get_value(table, "verbosity", self%verbosity, error, 'dependency_tree_t')
        if (allocated(error)) return
        call get_value(table, "ndep", self%ndep, error, 'dependency_tree_t')
        if (allocated(error)) return
        call get_value(table, "dep-dir", self%dep_dir)
        call get_value(table, "cache", self%cache)

        find_deps_table: do ii = 1, size(keys)
            if (keys(ii)%key=="dependencies") then

               call get_value(table, keys(ii), ptr_deps)
               if (.not.associated(ptr_deps)) then
                  call fatal_error(error,'dependency_tree_t: error retrieving dependency table from TOML table')
                  return
               end if

               !> Read all dependencies
               call ptr_deps%get_keys(dep_keys)
               call resize(self%dep, size(dep_keys))

               do jj = 1, size(dep_keys)

                   call get_value(ptr_deps, dep_keys(jj), ptr)
                   call self%dep(jj)%load_from_toml(ptr, error)
                   if (allocated(error)) return

               end do

               exit find_deps_table

            endif
        end do find_deps_table

    end subroutine tree_load_from_toml


end module fpm_dependency