fpm_build_lock.F90 Source File


Source Code

!> Lock on a build directory, so that fpm processes working on the same package
!> wait for each other instead of building over each other.
!>
!> The lock is an exclusive OS lock on `<build-dir>/.fpm-lock`: `flock` on Unix,
!> `LockFileEx` on Windows. It belongs to the process, and all its OpenMP threads
!> work under it: acquire and release it in serial code, outside any parallel region.
!> The OS releases it when the process exits, also after an error stop or a crash,
!> so no stale lock is left behind. In the bootstrap build, which has no C, it does
!> nothing.
module fpm_build_lock
    use, intrinsic :: iso_c_binding, only: c_char, c_int, c_intptr_t, c_null_char
    use, intrinsic :: iso_fortran_env, only: stderr => error_unit
    use fpm_filesystem, only: join_path, mkdir
    implicit none
    private

    public :: build_lock_t

    !> Lock on a build directory, held by the command that works on it. Release it
    !> on every way out of the command, before an error stop too. Do not copy a held
    !> lock: both copies would release it.
    type :: build_lock_t
        private
        !> Descriptor of the lock file (its HANDLE on Windows), -1 when not held
        integer(kind=c_intptr_t) :: handle = -1
    contains
        procedure :: acquire
        procedure :: release
        procedure :: held
        final :: release_on_exit
    end type build_lock_t

#ifndef FPM_BOOTSTRAP
    interface
        function c_lock_file(path, wait) result(handle) bind(c, name="c_lock_file")
            import c_char, c_int, c_intptr_t
            character(kind=c_char), intent(in) :: path(*)
            integer(kind=c_int), intent(in), value :: wait
            integer(kind=c_intptr_t) :: handle
        end function c_lock_file

        subroutine c_unlock_file(handle) bind(c, name="c_unlock_file")
            import c_intptr_t
            integer(kind=c_intptr_t), intent(in), value :: handle
        end subroutine c_unlock_file
    end interface
#endif

contains

    !> Lock the build directory `build_dir`, creating it if needed. While another
    !> fpm process holds the lock, say so and wait for it. If the lock cannot be
    !> taken, warn and go on without it. Does nothing if this lock is already held.
    subroutine acquire(self, build_dir)
        class(build_lock_t), intent(inout) :: self
        character(len=*), intent(in) :: build_dir
#ifndef FPM_BOOTSTRAP
        character(len=:), allocatable :: lock_file

        if (self%held()) return

        call mkdir(build_dir, echo=.false.)
        lock_file = join_path(build_dir, '.fpm-lock')

        self%handle = c_lock_file(lock_file//c_null_char, 0_c_int)
        if (self%handle == -2) then
            write(stderr, '(a)') '<INFO> Waiting for another fpm process to release '//lock_file
            self%handle = c_lock_file(lock_file//c_null_char, 1_c_int)
        end if
        if (self%handle < 0) then
            write(stderr, '(a)') '<WARN> Cannot lock '//lock_file// &
                                 ': fpm processes running at the same time on this package are not safe'
            self%handle = -1
        end if
#endif
    end subroutine acquire

    !> Release the lock, if held
    subroutine release(self)
        class(build_lock_t), intent(inout) :: self
#ifndef FPM_BOOTSTRAP
        if (.not. self%held()) return
        call c_unlock_file(self%handle)
        self%handle = -1
#endif
    end subroutine release

    !> Release the lock when it goes out of scope, as on a return from the command
    !> that holds it. Compilers that do not finalize local variables, like gfortran
    !> before 13, skip this, so commands release the lock themselves as well.
    subroutine release_on_exit(self)
        type(build_lock_t), intent(inout) :: self
        call self%release()
    end subroutine release_on_exit

    !> Whether this lock is held
    logical function held(self)
        class(build_lock_t), intent(in) :: self
        held = self%handle >= 0
    end function held

end module fpm_build_lock