Skip to content

Commit af67eae

Browse files
authored
Merge pull request #248 from LKedward/incremental2
Refactor backend for incremental rebuilds
2 parents 7d32029 + d57c591 commit af67eae

File tree

9 files changed

+701
-82
lines changed

9 files changed

+701
-82
lines changed

fpm/src/fpm.f90

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ module fpm
88
use fpm_model, only: fpm_model_t, srcfile_t, build_target_t, &
99
FPM_SCOPE_UNKNOWN, FPM_SCOPE_LIB, &
1010
FPM_SCOPE_DEP, FPM_SCOPE_APP, FPM_SCOPE_TEST, &
11-
FPM_TARGET_EXECUTABLE
11+
FPM_TARGET_EXECUTABLE, FPM_TARGET_ARCHIVE
1212

1313
use fpm_sources, only: add_executable_sources, add_sources_from_dir
14-
use fpm_targets, only: targets_from_sources, resolve_module_dependencies
14+
use fpm_targets, only: targets_from_sources, resolve_module_dependencies, &
15+
resolve_target_linking
1516
use fpm_manifest, only : get_package_data, package_config_t
1617
use fpm_error, only : error_t, fatal_error
1718
use fpm_manifest_test, only : test_config_t
@@ -240,8 +241,14 @@ subroutine build_model(model, settings, package, error)
240241
model%link_flags = model%link_flags // " -l" // model%link_libraries(i)%s
241242
end do
242243

244+
if (model%targets(1)%ptr%target_type == FPM_TARGET_ARCHIVE) then
245+
model%library_file = model%targets(1)%ptr%output_file
246+
end if
247+
243248
call resolve_module_dependencies(model%targets,error)
244249

250+
call resolve_target_linking(model%targets)
251+
245252
end subroutine build_model
246253

247254

@@ -403,13 +410,7 @@ subroutine cmd_run(settings,test)
403410

404411
end if
405412

406-
! NB. To be replaced after incremental rebuild is implemented
407-
if (.not.settings%list .and. &
408-
any([(.not.exists(executables(i)%s),i=1,size(executables))])) then
409-
410-
call build_package(model)
411-
412-
end if
413+
call build_package(model)
413414

414415
do i=1,size(executables)
415416
if (settings%list) then

fpm/src/fpm_backend.f90

Lines changed: 168 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,202 @@
1+
!> Implements the native fpm build backend
12
module fpm_backend
23

3-
! Implements the native fpm build backend
4-
5-
use fpm_environment, only: run, get_os_type, OS_WINDOWS
6-
use fpm_filesystem, only: basename, dirname, join_path, exists, mkdir
7-
use fpm_model, only: fpm_model_t, srcfile_t, build_target_t, FPM_UNIT_MODULE, &
8-
FPM_UNIT_SUBMODULE, FPM_UNIT_SUBPROGRAM, &
9-
FPM_UNIT_CSOURCE, FPM_UNIT_PROGRAM, &
10-
FPM_SCOPE_TEST, FPM_TARGET_OBJECT, FPM_TARGET_ARCHIVE, FPM_TARGET_EXECUTABLE
4+
use fpm_environment, only: run
5+
use fpm_filesystem, only: dirname, join_path, exists, mkdir
6+
use fpm_model, only: fpm_model_t, build_target_t, build_target_ptr, &
7+
FPM_TARGET_OBJECT, FPM_TARGET_ARCHIVE, FPM_TARGET_EXECUTABLE
118

12-
use fpm_strings, only: split
9+
use fpm_strings, only: string_cat
1310

1411
implicit none
1512

1613
private
17-
public :: build_package
14+
public :: build_package, sort_target, schedule_targets
1815

1916
contains
2017

21-
18+
!> Top-level routine to build package described by `model`
2219
subroutine build_package(model)
2320
type(fpm_model_t), intent(inout) :: model
2421

25-
integer :: i, ilib
26-
character(:), allocatable :: base, linking, subdir, link_flags
22+
integer :: i, j
23+
type(build_target_ptr), allocatable :: queue(:)
24+
integer, allocatable :: schedule_ptr(:)
2725

28-
if (.not.exists(model%output_directory)) then
29-
call mkdir(model%output_directory)
30-
end if
26+
! Need to make output directory for include (mod) files
3127
if (.not.exists(join_path(model%output_directory,model%package_name))) then
3228
call mkdir(join_path(model%output_directory,model%package_name))
3329
end if
3430

35-
if (model%targets(1)%ptr%target_type == FPM_TARGET_ARCHIVE) then
36-
linking = " "//model%targets(1)%ptr%output_file
37-
else
38-
linking = " "
39-
end if
40-
41-
linking = linking//" "//model%link_flags
42-
31+
! Perform depth-first topological sort of targets
4332
do i=1,size(model%targets)
4433

45-
call build_target(model,model%targets(i)%ptr,linking)
34+
call sort_target(model%targets(i)%ptr)
4635

4736
end do
4837

49-
end subroutine build_package
38+
! Construct build schedule queue
39+
call schedule_targets(queue, schedule_ptr, model%targets)
5040

41+
! Loop over parallel schedule regions
42+
do i=1,size(schedule_ptr)-1
5143

44+
! Build targets in schedule region i
45+
!$omp parallel do default(shared)
46+
do j=schedule_ptr(i),(schedule_ptr(i+1)-1)
47+
48+
call build_target(model,queue(j)%ptr)
49+
50+
end do
51+
52+
end do
53+
54+
end subroutine build_package
5255

53-
recursive subroutine build_target(model,target,linking)
54-
! Compile Fortran source, called recursively on it dependents
55-
!
56-
type(fpm_model_t), intent(in) :: model
57-
type(build_target_t), intent(inout) :: target
58-
character(:), allocatable, intent(in) :: linking
5956

60-
integer :: i, j, ilib
57+
!> Topologically sort a target for scheduling by
58+
!> recursing over its dependencies.
59+
!>
60+
!> Checks disk-cached source hashes to determine if objects are
61+
!> up-to-date. Up-to-date sources are tagged as skipped.
62+
!>
63+
recursive subroutine sort_target(target)
64+
type(build_target_t), intent(inout), target :: target
65+
66+
integer :: i, j, fh, stat
6167
type(build_target_t), pointer :: exe_obj
62-
character(:), allocatable :: objs, link_flags
6368

64-
if (target%built) then
69+
! Check if target has already been processed (as a dependency)
70+
if (target%sorted .or. target%skip) then
6571
return
6672
end if
6773

74+
! Check for a circular dependency
75+
! (If target has been touched but not processed)
6876
if (target%touched) then
6977
write(*,*) '(!) Circular dependency found with: ',target%output_file
7078
stop
7179
else
72-
target%touched = .true.
80+
target%touched = .true. ! Set touched flag
7381
end if
7482

75-
objs = " "
83+
! Load cached source file digest if present
84+
if (.not.allocated(target%digest_cached) .and. &
85+
exists(target%output_file) .and. &
86+
exists(target%output_file//'.digest')) then
7687

77-
do i=1,size(target%dependencies)
88+
allocate(target%digest_cached)
89+
open(newunit=fh,file=target%output_file//'.digest',status='old')
90+
read(fh,*,iostat=stat) target%digest_cached
91+
close(fh)
7892

79-
if (associated(target%dependencies(i)%ptr)) then
80-
call build_target(model,target%dependencies(i)%ptr,linking)
93+
if (stat /= 0) then ! Cached digest is not recognized
94+
deallocate(target%digest_cached)
8195
end if
8296

83-
if (target%target_type == FPM_TARGET_ARCHIVE ) then
97+
end if
98+
99+
if (allocated(target%source)) then
84100

85-
! Construct object list for archive
86-
objs = objs//" "//target%dependencies(i)%ptr%output_file
101+
! Skip if target is source-based and source file is unmodified
102+
if (allocated(target%digest_cached)) then
103+
if (target%digest_cached == target%source%digest) target%skip = .true.
104+
end if
87105

88-
else if (target%target_type == FPM_TARGET_EXECUTABLE .and. &
89-
target%dependencies(i)%ptr%target_type == FPM_TARGET_OBJECT) then
106+
elseif (exists(target%output_file)) then
90107

91-
exe_obj => target%dependencies(i)%ptr
92-
93-
! Construct object list for executable
94-
objs = " "//exe_obj%output_file
95-
96-
! Include non-library object dependencies
97-
do j=1,size(exe_obj%dependencies)
108+
! Skip if target is not source-based and already exists
109+
target%skip = .true.
98110

99-
if (allocated(exe_obj%dependencies(j)%ptr%source)) then
100-
if (exe_obj%dependencies(j)%ptr%source%unit_scope == exe_obj%source%unit_scope) then
101-
objs = objs//" "//exe_obj%dependencies(j)%ptr%output_file
102-
end if
103-
end if
111+
end if
104112

105-
end do
113+
! Loop over target dependencies
114+
target%schedule = 1
115+
do i=1,size(target%dependencies)
116+
117+
! Sort dependency
118+
call sort_target(target%dependencies(i)%ptr)
119+
120+
if (.not.target%dependencies(i)%ptr%skip) then
121+
122+
! Can't skip target if any dependency is not skipped
123+
target%skip = .false.
124+
125+
! Set target schedule after all of its dependencies
126+
target%schedule = max(target%schedule,target%dependencies(i)%ptr%schedule+1)
106127

107128
end if
108129

109130
end do
110131

132+
! Mark flag as processed: either sorted or skipped
133+
target%sorted = .not.target%skip
134+
135+
end subroutine sort_target
136+
137+
138+
!> Construct a build schedule from the sorted targets.
139+
!>
140+
!> The schedule is broken into regions, described by `schedule_ptr`,
141+
!> where targets in each region can be compiled in parallel.
142+
!>
143+
subroutine schedule_targets(queue, schedule_ptr, targets)
144+
type(build_target_ptr), allocatable, intent(out) :: queue(:)
145+
integer, allocatable :: schedule_ptr(:)
146+
type(build_target_ptr), intent(in) :: targets(:)
147+
148+
integer :: i, j
149+
integer :: n_schedule, n_sorted
150+
151+
n_schedule = 0 ! Number of schedule regions
152+
n_sorted = 0 ! Total number of targets to build
153+
do i=1,size(targets)
154+
155+
if (targets(i)%ptr%sorted) then
156+
n_sorted = n_sorted + 1
157+
end if
158+
n_schedule = max(n_schedule, targets(i)%ptr%schedule)
159+
160+
end do
161+
162+
allocate(queue(n_sorted))
163+
allocate(schedule_ptr(n_schedule+1))
164+
165+
! Construct the target queue and schedule region pointer
166+
n_sorted = 1
167+
schedule_ptr(n_sorted) = 1
168+
do i=1,n_schedule
169+
170+
do j=1,size(targets)
171+
172+
if (targets(j)%ptr%sorted) then
173+
if (targets(j)%ptr%schedule == i) then
174+
175+
queue(n_sorted)%ptr => targets(j)%ptr
176+
n_sorted = n_sorted + 1
177+
end if
178+
end if
179+
180+
end do
181+
182+
schedule_ptr(i+1) = n_sorted
183+
184+
end do
185+
186+
end subroutine schedule_targets
187+
188+
189+
!> Call compile/link command for a single target.
190+
!>
191+
!> If successful, also caches the source file digest to disk.
192+
!>
193+
subroutine build_target(model,target)
194+
type(fpm_model_t), intent(in) :: model
195+
type(build_target_t), intent(in), target :: target
196+
197+
integer :: ilib, fh
198+
character(:), allocatable :: link_flags
199+
111200
if (.not.exists(dirname(target%output_file))) then
112201
call mkdir(dirname(target%output_file))
113202
end if
@@ -119,22 +208,34 @@ recursive subroutine build_target(model,target,linking)
119208
// " -o " // target%output_file)
120209

121210
case (FPM_TARGET_EXECUTABLE)
122-
link_flags = linking
211+
212+
link_flags = string_cat(target%link_objects," ")
213+
214+
if (allocated(model%library_file)) then
215+
link_flags = link_flags//" "//model%library_file//" "//model%link_flags
216+
else
217+
link_flags = link_flags//" "//model%link_flags
218+
end if
219+
123220
if (allocated(target%link_libraries)) then
124-
do ilib = 1, size(target%link_libraries)
125-
link_flags = link_flags // " -l" // target%link_libraries(ilib)%s
126-
end do
221+
if (size(target%link_libraries) > 0) then
222+
link_flags = link_flags // " -l" // string_cat(target%link_libraries," -l")
223+
end if
127224
end if
128-
129-
call run("gfortran " // objs // model%fortran_compile_flags &
130-
//link_flags// " -o " // target%output_file)
225+
226+
call run("gfortran " // model%fortran_compile_flags &
227+
//" "//link_flags// " -o " // target%output_file)
131228

132229
case (FPM_TARGET_ARCHIVE)
133-
call run("ar -rs " // target%output_file // objs)
230+
call run("ar -rs " // target%output_file // " " // string_cat(target%link_objects," "))
134231

135232
end select
136233

137-
target%built = .true.
234+
if (allocated(target%source)) then
235+
open(newunit=fh,file=target%output_file//'.digest',status='unknown')
236+
write(fh,*) target%source%digest
237+
close(fh)
238+
end if
138239

139240
end subroutine build_target
140241

fpm/src/fpm_model.f90

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module fpm_model
22
! Definition and validation of the backend model
3+
use iso_fortran_env, only: int64
34
use fpm_strings, only: string_t
45
implicit none
56

@@ -53,6 +54,8 @@ module fpm_model
5354
! Files INCLUDEd by this source file
5455
type(string_t), allocatable :: link_libraries(:)
5556
! Native libraries to link against
57+
integer(int64) :: digest
58+
! Current hash
5659
end type srcfile_t
5760

5861
type build_target_ptr
@@ -70,9 +73,20 @@ module fpm_model
7073
integer :: target_type = FPM_TARGET_UNKNOWN
7174
type(string_t), allocatable :: link_libraries(:)
7275
! Native libraries to link against
76+
type(string_t), allocatable :: link_objects(:)
77+
! Objects needed to link this target
7378

74-
logical :: built = .false.
7579
logical :: touched = .false.
80+
! Flag set when first visited to check for circular dependencies
81+
logical :: sorted = .false.
82+
! Flag set if build target is sorted for building
83+
logical :: skip = .false.
84+
! Flag set if build target will be skipped (not built)
85+
86+
integer :: schedule = -1
87+
! Targets in the same schedule group are guaranteed to be independent
88+
integer(int64), allocatable :: digest_cached
89+
! Previous hash
7690

7791
end type build_target_t
7892

@@ -89,6 +103,8 @@ module fpm_model
89103
! Command line flags passed to fortran for compilation
90104
character(:), allocatable :: link_flags
91105
! Command line flags pass for linking
106+
character(:), allocatable :: library_file
107+
! Output file for library archive
92108
character(:), allocatable :: output_directory
93109
! Base directory for build
94110
type(string_t), allocatable :: link_libraries(:)

0 commit comments

Comments
 (0)