Skip to content

Commit 135bfbb

Browse files
authored
feat: add profile field to dependency configuration (#1243)
2 parents 3b7a094 + d3114b7 commit 135bfbb

File tree

7 files changed

+194
-6
lines changed

7 files changed

+194
-6
lines changed

ci/test_features.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,16 @@ grep -q "WITH_DEBUG_DEPENDENCY" output.txt || { echo "ERROR: WITH_DEBUG_DEPENDEN
157157
grep -q "DEBUG mode enabled" output.txt || { echo "ERROR: DEBUG mode not enabled in dependency profile test"; exit 1; }
158158
echo "✓ Debug dependency profile works"
159159

160+
# Test 15: Dependency with profile (instead of features list)
161+
# features_demo's "prof_feats" profile resolves to ["prof_feat1", "prof_feat2"],
162+
# which sets the PROF_FEAT1 and PROF_FEAT2 macros
163+
echo "Test 15: Dependency with profile"
164+
rm -rf build
165+
"$fpm" run --profile dep_profile > output.txt
166+
grep -q "PROF_FEAT1 enabled" output.txt || { echo "ERROR: PROF_FEAT1 not enabled via dependency profile=prof_feats"; exit 1; }
167+
grep -q "PROF_FEAT2 enabled" output.txt || { echo "ERROR: PROF_FEAT2 not enabled via dependency profile=prof_feats"; exit 1; }
168+
echo "Dependency profile works"
169+
160170
# Cleanup
161171
rm -rf build output.txt
162172
popd

example_packages/features_demo/fpm.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ mpi.dependencies.mpi = "*"
3030
openmp.preprocess.cpp.macros = "USE_OPENMP"
3131
openmp.dependencies.openmp = "*"
3232

33+
# Features for testing dependency profile resolution
34+
prof_feat1.preprocess.cpp.macros = "PROF_FEAT1"
35+
prof_feat2.preprocess.cpp.macros = "PROF_FEAT2"
36+
3337
[profiles]
3438
development = ["debug"]
3539
production = ["release", "openmp"]
40+
prof_feats = ["prof_feat1", "prof_feat2"]

example_packages/features_demo/src/features_demo.f90

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ subroutine show_features()
3434
write(*,*) '✓ OpenMP support enabled'
3535
#endif
3636

37+
#ifdef PROF_FEAT1
38+
write(*,*) '✓ PROF_FEAT1 enabled'
39+
#endif
40+
#ifdef PROF_FEAT2
41+
write(*,*) '✓ PROF_FEAT2 enabled'
42+
#endif
43+
3744
! Compiler info (if available)
3845
write(*,*) 'Build configuration:'
3946
call show_compiler_info()

example_packages/features_with_dependency/fpm.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,14 @@ gfortran_optimized.gfortran.dependencies.features_demo = { path = "../features_d
3333
gfortran_optimized.gfortran.flags = "-O3 -mtune=generic -funroll-loops"
3434
gfortran_optimized.preprocess.cpp.macros = ["WITH_DEMO"]
3535

36+
# Feature that uses a profile name (instead of features list) to build the dependency
37+
# "prof_feats" profile in features_demo resolves to ["prof_feat1", "prof_feat2"],
38+
# which sets the PROF_FEAT1 and PROF_FEAT2 macros
39+
with_dep_profile.dependencies.features_demo = { path = "../features_demo", profile = "prof_feats" }
40+
with_dep_profile.preprocess.cpp.macros = ["WITH_DEMO"]
3641

3742
[profiles]
3843
debug_dep = ["with_feat_debug"]
3944
release_dep = ["with_feat_release"]
4045
full_test = ["with_feat_multi", "linux_specific"]
46+
dep_profile = ["with_dep_profile"]

src/fpm.f90

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ subroutine build_model(model, settings, package_config, error)
159159

160160
! Adapt it to the current profile/platform
161161
dependency = dependency_config%export_config(target_platform, &
162-
dep%features,verbose=.false.,error=error)
162+
dep%features,dep%profile,verbose=.false.,error=error)
163163
if (allocated(error)) exit
164164

165165
manifest => dependency

src/fpm/manifest/dependency.f90

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,14 @@
99
!>"dep3" = { git = "url", tag = "name" }
1010
!>"dep4" = { git = "url", rev = "sha1" }
1111
!>"dep0" = { path = "path" }
12+
!>"dep5" = { path = "path", features = ["feat1", "feat2"] }
13+
!>"dep6" = { path = "path", profile = "myprofile" }
1214
!>```
1315
!>
16+
!> The `features` and `profile` keys are mutually exclusive.
17+
!> `features` provides a list of individual feature names;
18+
!> `profile` provides a single profile name defined in the dependency's manifest.
19+
!>
1420
!> To reduce the amount of boilerplate code this module provides two constructors
1521
!> for dependency types, one basic for an actual dependency (inline) table
1622
!> and another to collect all dependency objects from a dependencies table,
@@ -66,6 +72,9 @@ module fpm_manifest_dependency
6672
!> Requested features for the dependency
6773
type(string_t), allocatable :: features(:)
6874

75+
!> Requested profile for the dependency (mutually exclusive with features)
76+
character(len=:), allocatable :: profile
77+
6978
!> Git descriptor
7079
type(git_target_t), allocatable :: git
7180

@@ -136,6 +145,9 @@ subroutine new_dependency(self, table, root, error)
136145
call get_list(table, "features", self%features, error)
137146
if (allocated(error)) return
138147

148+
!> Get optional profile name
149+
call get_value(table, "profile", self%profile)
150+
139151
call get_value(table, "path", uri)
140152
if (allocated(uri)) then
141153
if (get_os_type() == OS_WINDOWS) uri = windows_path(uri)
@@ -197,7 +209,8 @@ subroutine check(table, error)
197209
"branch", &
198210
"rev", &
199211
"preprocess", &
200-
"features" &
212+
"features", &
213+
"profile" &
201214
& ]
202215

203216
call table%get_key(name)
@@ -252,7 +265,13 @@ subroutine check(table, error)
252265
end if
253266

254267
end if
255-
268+
269+
! Check that features and profile are not both specified
270+
if (table%has_key('features') .and. table%has_key('profile')) then
271+
call syntax_error(error, "Dependency '"//name//"' cannot have both 'features' and 'profile' entries")
272+
return
273+
end if
274+
256275
end subroutine check
257276

258277
!> Construct new dependency array from a TOML data structure
@@ -381,6 +400,10 @@ subroutine info(self, unit, verbosity)
381400
end do
382401
end if
383402

403+
if (allocated(self%profile)) then
404+
write(unit, fmt) " - profile", self%profile
405+
end if
406+
384407
end subroutine info
385408

386409
!> Check if two dependency configurations are different
@@ -418,6 +441,7 @@ elemental subroutine dependency_destroy(self)
418441
if (allocated(self%requested_version)) deallocate(self%requested_version)
419442
if (allocated(self%git)) deallocate(self%git)
420443
if (allocated(self%features)) deallocate(self%features)
444+
if (allocated(self%profile)) deallocate(self%profile)
421445

422446
end subroutine dependency_destroy
423447

@@ -450,7 +474,11 @@ logical function dependency_is_same(this,that)
450474
if (allocated(this%features).neqv.allocated(other%features)) return
451475
if (allocated(this%features)) then
452476
if (.not.(this%features==other%features)) return
453-
endif
477+
endif
478+
if (allocated(this%profile).neqv.allocated(other%profile)) return
479+
if (allocated(this%profile)) then
480+
if (.not.(this%profile==other%profile)) return
481+
endif
454482

455483
if ((allocated(this%git).neqv.allocated(other%git))) return
456484
if (allocated(this%git)) then
@@ -493,7 +521,9 @@ subroutine dump_to_toml(self, table, error)
493521
if (allocated(error)) return
494522
endif
495523
call set_list(table, "features", self%features, error)
496-
if (allocated(error)) return
524+
if (allocated(error)) return
525+
call set_string(table, "profile", self%profile, error, 'dependency_config_t')
526+
if (allocated(error)) return
497527

498528
if (allocated(self%git)) then
499529
call add_table(table, "git", ptr, error)
@@ -537,7 +567,8 @@ subroutine load_from_toml(self, table, error)
537567
endif
538568
end if
539569
call get_list(table, "features", self%features, error)
540-
if (allocated(error)) return
570+
if (allocated(error)) return
571+
call get_value(table, "profile", self%profile)
541572

542573
call table%get_keys(list)
543574
add_git: do ii = 1, size(list)

test/fpm_test/test_manifest.f90

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ subroutine collect_manifest(testsuite)
4141
& new_unittest("dependency-features-present", test_dependency_features_present), &
4242
& new_unittest("dependency-features-absent", test_dependency_features_absent), &
4343
& new_unittest("dependency-features-empty", test_dependency_features_empty), &
44+
& new_unittest("dependency-profile-present", test_dependency_profile_present), &
45+
& new_unittest("dependency-profile-absent", test_dependency_profile_absent), &
46+
& new_unittest("dependency-profile-features-conflict", &
47+
& test_dependency_profile_features_conflict, should_fail=.true.), &
4448
& new_unittest("dependency-wrongkey", test_dependency_wrongkey, should_fail=.true.), &
4549
& new_unittest("dependencies-empty", test_dependencies_empty), &
4650
& new_unittest("dependencies-typeerror", test_dependencies_typeerror, should_fail=.true.), &
@@ -1809,4 +1813,129 @@ subroutine test_features_demo_serialization(error)
18091813
end subroutine test_features_demo_serialization
18101814

18111815

1816+
!> Test that a dependency with "profile" key is parsed correctly
1817+
subroutine test_dependency_profile_present(error)
1818+
!> Error handling
1819+
type(error_t), allocatable, intent(out) :: error
1820+
1821+
type(package_config_t) :: package
1822+
character(:), allocatable :: temp_file
1823+
integer :: unit, i, idx_dep0, idx_dep1
1824+
1825+
allocate(temp_file, source=get_temp_filename())
1826+
1827+
open(file=temp_file, newunit=unit)
1828+
write(unit, '(a)') &
1829+
& 'name = "example"', &
1830+
& 'version = "0.1.0"', &
1831+
& '[dependencies]', &
1832+
& '"dep0" = { path = "local/dep0", profile = "release" }', &
1833+
& '"dep1" = { path = "local/dep1" }'
1834+
close(unit)
1835+
1836+
call get_package_data(package, temp_file, error)
1837+
if (allocated(error)) return
1838+
1839+
if (.not.allocated(package%dependency)) then
1840+
call test_failed(error, 'No dependencies parsed from manifest')
1841+
return
1842+
end if
1843+
1844+
idx_dep0 = 0; idx_dep1 = 0
1845+
do i = 1, size(package%dependency)
1846+
select case (package%dependency(i)%name)
1847+
case ('dep0'); idx_dep0 = i
1848+
case ('dep1'); idx_dep1 = i
1849+
end select
1850+
end do
1851+
1852+
if (idx_dep0 == 0 .or. idx_dep1 == 0) then
1853+
call test_failed(error, 'Expected dependencies dep0/dep1 not found')
1854+
return
1855+
end if
1856+
1857+
! dep0: profile = "release"
1858+
if (.not.allocated(package%dependency(idx_dep0)%profile)) then
1859+
call test_failed(error, 'dep0 profile not allocated')
1860+
return
1861+
end if
1862+
if (package%dependency(idx_dep0)%profile /= 'release') then
1863+
call test_failed(error, 'dep0 profile value mismatch, expected "release"')
1864+
return
1865+
end if
1866+
! dep0 should NOT have features allocated
1867+
if (allocated(package%dependency(idx_dep0)%features)) then
1868+
if (size(package%dependency(idx_dep0)%features) > 0) then
1869+
call test_failed(error, 'dep0 features should not be present when profile is used')
1870+
return
1871+
end if
1872+
end if
1873+
1874+
! dep1: no profile key -> should be NOT allocated
1875+
if (allocated(package%dependency(idx_dep1)%profile)) then
1876+
call test_failed(error, 'dep1 profile should be unallocated when key is absent')
1877+
return
1878+
end if
1879+
end subroutine test_dependency_profile_present
1880+
1881+
1882+
!> Ensure a dependency without "profile" key leaves it unallocated
1883+
subroutine test_dependency_profile_absent(error)
1884+
!> Error handling
1885+
type(error_t), allocatable, intent(out) :: error
1886+
1887+
type(package_config_t) :: package
1888+
character(:), allocatable :: temp_file
1889+
integer :: unit, i
1890+
1891+
allocate(temp_file, source=get_temp_filename())
1892+
1893+
open(file=temp_file, newunit=unit)
1894+
write(unit, '(a)') &
1895+
& 'name = "example"', &
1896+
& '[dependencies]', &
1897+
& '"a" = { path = "a" }', &
1898+
& '"b" = { git = "https://example.org/b.git", branch = "main" }'
1899+
close(unit)
1900+
1901+
call get_package_data(package, temp_file, error)
1902+
if (allocated(error)) return
1903+
1904+
if (.not.allocated(package%dependency)) then
1905+
call test_failed(error, 'No dependencies parsed from manifest')
1906+
return
1907+
end if
1908+
1909+
do i = 1, size(package%dependency)
1910+
if (allocated(package%dependency(i)%profile)) then
1911+
call test_failed(error, 'profile should be unallocated when not specified')
1912+
return
1913+
end if
1914+
end do
1915+
end subroutine test_dependency_profile_absent
1916+
1917+
1918+
!> Specifying both "features" and "profile" should be an error
1919+
subroutine test_dependency_profile_features_conflict(error)
1920+
!> Error handling
1921+
type(error_t), allocatable, intent(out) :: error
1922+
1923+
type(package_config_t) :: package
1924+
character(:), allocatable :: temp_file
1925+
integer :: unit
1926+
1927+
allocate(temp_file, source=get_temp_filename())
1928+
1929+
open(file=temp_file, newunit=unit)
1930+
write(unit, '(a)') &
1931+
& 'name = "example"', &
1932+
& '[dependencies]', &
1933+
& '"bad" = { path = "bad", features = ["f1"], profile = "release" }'
1934+
close(unit)
1935+
1936+
call get_package_data(package, temp_file, error)
1937+
1938+
end subroutine test_dependency_profile_features_conflict
1939+
1940+
18121941
end module test_manifest

0 commit comments

Comments
 (0)