diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 3db6dc91aa..fcca8e33dc 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -29,12 +29,12 @@ jobs: - {compiler: gcc, version: 13} - {compiler: gcc, version: 14} - {compiler: gcc, version: 15} - - {compiler: intel, version: 2025.1} + - {compiler: intel, version: 2025.2} exclude: - os: macos-13 # No Intel on MacOS anymore since 2024 - toolchain: {compiler: intel, version: '2025.1'} + toolchain: {compiler: intel, version: '2025.2'} - os: windows-latest # Doesn't pass build and tests yet - toolchain: {compiler: intel, version: '2025.1'} + toolchain: {compiler: intel, version: '2025.2'} - os: windows-latest # gcc 14 not available on Windows yet toolchain: {compiler: gcc, version: 14} - os: windows-latest # gcc 15 not available on Windows yet diff --git a/ci/meta_tests.sh b/ci/meta_tests.sh index 6c1754e491..f0c9eb53db 100755 --- a/ci/meta_tests.sh +++ b/ci/meta_tests.sh @@ -28,8 +28,8 @@ pushd metapackage_stdlib popd pushd metapackage_minpack -"$fpm" build --verbose -"$fpm" run --verbose +"$fpm" build --verbose --flag " -Wno-external-argument-mismatch" +"$fpm" run --verbose --flag " -Wno-external-argument-mismatch" popd pushd metapackage_mpi diff --git a/ci/run_tests.sh b/ci/run_tests.sh index b1acb3bf60..239ccc49cf 100755 --- a/ci/run_tests.sh +++ b/ci/run_tests.sh @@ -317,7 +317,7 @@ popd # Test shared library dependencies pushd shared_lib -"$fpm" build || EXIT_CODE=$? +"$fpm" build --verbose || EXIT_CODE=$? test $EXIT_CODE -eq 0 popd @@ -374,5 +374,9 @@ popd # Test custom build directory functionality bash "../ci/test_custom_build_dir.sh" "$fpm" hello_world +# Test FPM features functionality +echo "=== Testing FPM Features Functionality ===" +bash "../ci/test_features.sh" "$fpm" + # Cleanup rm -rf ./*/build diff --git a/ci/test_features.sh b/ci/test_features.sh new file mode 100755 index 0000000000..387f26c57e --- /dev/null +++ b/ci/test_features.sh @@ -0,0 +1,314 @@ +#!/usr/bin/env bash +set -exo pipefail + +# Test script for FPM features functionality +# Usage: ./test_features.sh [fpm_executable] +# Note: This script should be run from the repo root or integrated into run_tests.sh + +if [ "$1" ]; then + fpm="$1" +else + # Default to the fpm passed from run_tests.sh or system fpm + fpm="${fpm:-fpm}" +fi + +echo "Testing FPM features functionality" + +echo "=== Testing features_demo package ===" + +# Test 1: Basic features - debug feature +pushd "features_demo" +echo "Test 1: Basic debug feature" +rm -rf build +"$fpm" run --features debug > output.txt +grep -q "DEBUG mode enabled" output.txt || { echo "ERROR: DEBUG mode not enabled"; exit 1; } +echo "✓ Debug feature works" + +# Test 2: Profile usage - development profile (includes debug) +echo "Test 2: Development profile (debug feature)" +rm -rf build +"$fpm" run --profile development --target features_demo > output.txt +grep -q "DEBUG mode enabled" output.txt || { echo "ERROR: DEBUG mode not enabled in development profile"; exit 1; } +echo "✓ Development profile works" + +# Test 3: Multiple features +echo "Test 3: Multiple features (debug + openmp)" +rm -rf build +"$fpm" run --features debug,openmp --target features_demo > output.txt +grep -q "DEBUG mode enabled" output.txt || { echo "ERROR: DEBUG mode not enabled with multiple features"; exit 1; } +grep -q "OpenMP support enabled" output.txt || { echo "ERROR: OpenMP not enabled with multiple features"; exit 1; } +echo "✓ Multiple features work" + +# Test 4: Feature-specific executable (debug_demo only available with debug feature) +echo "Test 4: Feature-specific executable" +rm -rf build +"$fpm" run --features debug --target debug_demo > output.txt +grep -q "Debug Demo Program" output.txt || { echo "ERROR: Debug Demo Program not found"; exit 1; } +grep -q "Debug mode: ON" output.txt || { echo "ERROR: Debug mode not ON in debug_demo"; exit 1; } +echo "✓ Feature-specific executable works" + +# Test 5: Profile with multiple features - production profile (release + openmp) +echo "Test 5: Production profile (release + openmp)" +rm -rf build +"$fpm" run --profile production --target features_demo > output.txt +grep -q "RELEASE mode enabled" output.txt || { echo "ERROR: RELEASE mode not enabled in production profile"; exit 1; } +grep -q "OpenMP support enabled" output.txt || { echo "ERROR: OpenMP not enabled in production profile"; exit 1; } +# Should NOT have debug +if grep -q "DEBUG mode enabled" output.txt; then + echo "ERROR: DEBUG mode should not be enabled in production profile" + exit 1 +fi +echo "✓ Production profile works" + +# Test 6: No features - baseline behavior +echo "Test 6: No features (baseline)" +rm -rf build +"$fpm" run --target features_demo > output.txt +# Should have neither DEBUG nor RELEASE without explicit features +if grep -q "DEBUG mode enabled" output.txt; then + echo "ERROR: DEBUG mode should not be enabled in baseline" + exit 1 +fi +if grep -q "RELEASE mode enabled" output.txt; then + echo "ERROR: RELEASE mode should not be enabled in baseline" + exit 1 +fi +if ! grep -q "Features: NONE" output.txt && ! grep -q "Demo completed successfully" output.txt; then + echo "ERROR: Expected baseline features output not found" + exit 1 +fi +echo "✓ Baseline (no features) works" + +# Test 7: Error handling - invalid feature +echo "Test 7: Error handling for invalid feature" +rm -rf build +if ! "$fpm" run --features nonexistent --target features_demo > /dev/null 2>&1; then + echo "Correctly rejected invalid feature" +else + echo "ERROR: Should reject invalid feature" && exit 1 +fi + +# Test 8: Error handling - invalid profile +echo "Test 8: Error handling for invalid profile" +rm -rf build +if ! "$fpm" run --profile nonexistent --target features_demo > /dev/null 2>&1; then + echo "Correctly rejected invalid profile" +else + echo "ERROR: Should reject invalid profile" && exit 1 +fi + +# Test 9: Features and profile mutual exclusion +echo "Test 9: Features and profile mutual exclusion" +rm -rf build +if ! "$fpm" run --features debug --profile development --target features_demo > /dev/null 2>&1; then + echo "Correctly rejected features + profile combination" +else + echo "ERROR: Should reject features + profile combination" && exit 1 +fi + +# Cleanup +rm -rf build output.txt build_list.txt +popd + +echo "=== Testing features_with_dependency package ===" + +# Test dependency features +pushd "features_with_dependency" + +# RE-ENABLE AFTER MERGING fpm WITH CPP PARSING PR +# Test 10: No features - should show NONE for both local and dependency +# echo "Test 10: Dependency package without features" +rm -rf build +#"$fpm" run | tee output.txt +# grep -q "NONE - no local features active" output.txt || { echo "ERROR: Local features NONE message not found"; exit 1; } +# grep -q "Features: NONE" output.txt || { echo "ERROR: Features NONE not found in dependency test"; exit 1; } +# echo "✓ Dependency package baseline works" + +# Test 11: Debug dependency feature +echo "Test 11: Debug dependency feature" +rm -rf build +"$fpm" run --features with_feat_debug > output.txt +grep -q "WITH_DEBUG_DEPENDENCY" output.txt || { echo "ERROR: WITH_DEBUG_DEPENDENCY not found"; exit 1; } +grep -q "DEBUG mode enabled" output.txt || { echo "ERROR: DEBUG mode not enabled in dependency test"; exit 1; } +echo "✓ Debug dependency feature works" + +# Test 12: Release dependency feature +echo "Test 12: Release dependency feature" +rm -rf build +"$fpm" run --features with_feat_release > output.txt +grep -q "WITH_RELEASE_DEPENDENCY" output.txt || { echo "ERROR: WITH_RELEASE_DEPENDENCY not found"; exit 1; } +grep -q "RELEASE mode enabled" output.txt || { echo "ERROR: RELEASE mode not enabled in dependency test"; exit 1; } +echo "✓ Release dependency feature works" + +# Test 13: Multi dependency feature +echo "Test 13: Multi dependency feature" +rm -rf build +"$fpm" run --features with_feat_multi > output.txt +grep -q "WITH_MULTI_DEPENDENCY" output.txt || { echo "ERROR: WITH_MULTI_DEPENDENCY not found"; exit 1; } +grep -q "DEBUG mode enabled" output.txt || { echo "ERROR: DEBUG mode not enabled in multi dependency test"; exit 1; } +grep -q "MPI support enabled" output.txt || { echo "ERROR: MPI support not enabled in multi dependency test"; exit 1; } +echo "✓ Multi dependency feature works" + +# Test 14: Profile with dependency features +echo "Test 14: Debug dependency profile" +rm -rf build +"$fpm" run --profile debug_dep > output.txt +grep -q "WITH_DEBUG_DEPENDENCY" output.txt || { echo "ERROR: WITH_DEBUG_DEPENDENCY not found in profile test"; exit 1; } +grep -q "DEBUG mode enabled" output.txt || { echo "ERROR: DEBUG mode not enabled in dependency profile test"; exit 1; } +echo "✓ Debug dependency profile works" + +# Cleanup +rm -rf build output.txt +popd + +echo "=== Testing features_per_compiler package ===" + +# Test features per compiler package +pushd "features_per_compiler" + +# Test 15: Development profile (debug + verbose) +echo "Test 15: Features per compiler - development profile" +rm -rf build +if "$fpm" run --profile development > output.txt 2>&1; then + echo "✓ Exit code 0 (success) as expected" +else + echo "ERROR: Expected exit code 0 but got non-zero exit code" + echo "=== Program output ===" + cat output.txt + echo "======================" + exit 1 +fi +grep -q "Features Per Compiler Demo" output.txt || { echo "ERROR: Features Per Compiler Demo not found"; exit 1; } +grep -q "✓ DEBUG: debug flags found" output.txt || { echo "ERROR: Debug feature not detected"; exit 1; } +grep -q "✓ VERBOSE: -v flag found" output.txt || { echo "ERROR: Verbose feature not detected"; exit 1; } +grep -q "✓ All compiler flag checks PASSED" output.txt || { echo "ERROR: Expected all checks to pass"; exit 1; } +# Check compiler-specific flags (will depend on detected compiler) +if grep -q "Detected compiler: gfortran" output.txt; then + grep -q "✓ Debug: -Wall found" output.txt || { echo "ERROR: gfortran debug flag -Wall not found"; exit 1; } + grep -q "✓ Debug: -fcheck=bounds found" output.txt || { echo "ERROR: gfortran debug flag -fcheck=bounds not found"; exit 1; } +fi +echo "✓ Development profile works" + +# Test 16: Production profile (release + fast) +echo "Test 16: Features per compiler - production profile" +rm -rf build +if "$fpm" run --profile production > output.txt 2>&1; then + echo "✓ Exit code 0 (success) as expected" +else + echo "ERROR: Expected exit code 0 but got non-zero exit code" + echo "=== Program output ===" + cat output.txt + echo "======================" + exit 1 +fi +grep -q "Features Per Compiler Demo" output.txt || { echo "ERROR: Features Per Compiler Demo not found"; exit 1; } +grep -q "✓ RELEASE: -O flags found" output.txt || { echo "ERROR: Release feature not detected"; exit 1; } +grep -q "✓ FAST: fast optimization flags found" output.txt || { echo "ERROR: Fast feature not detected"; exit 1; } +grep -q "✓ All compiler flag checks PASSED" output.txt || { echo "ERROR: Expected all checks to pass"; exit 1; } +# Check compiler-specific flags (will depend on detected compiler) +if grep -q "Detected compiler: gfortran" output.txt; then + # Check for either -march=native or -mcpu (Apple Silicon uses -mcpu) + if ! (grep -q "✓ Release: -march=native found" output.txt || grep -q "✓ Release: -mcpu found" output.txt); then + echo "ERROR: gfortran release architecture flag (-march=native or -mcpu) not found" + exit 1 + fi + grep -q "✓ Fast: -ffast-math found" output.txt || { echo "ERROR: gfortran fast flag -ffast-math not found"; exit 1; } +fi +echo "✓ Production profile works" + +# Test 17: Testing profile (debug + strict) +echo "Test 17: Features per compiler - testing profile" +rm -rf build +if "$fpm" run --profile testing > output.txt 2>&1; then + echo "✓ Exit code 0 (success) as expected" +else + echo "ERROR: Expected exit code 0 but got non-zero exit code" + echo "=== Program output ===" + cat output.txt + echo "======================" + exit 1 +fi +grep -q "Features Per Compiler Demo" output.txt || { echo "ERROR: Features Per Compiler Demo not found"; exit 1; } +grep -q "✓ DEBUG: debug flags found" output.txt || { echo "ERROR: Debug feature not detected"; exit 1; } +grep -q "✓ STRICT: standard compliance flags found" output.txt || { echo "ERROR: Strict feature not detected"; exit 1; } +grep -q "✓ All compiler flag checks PASSED" output.txt || { echo "ERROR: Expected all checks to pass"; exit 1; } +# Check compiler-specific flags (will depend on detected compiler) +if grep -q "Detected compiler: gfortran" output.txt; then + grep -q "✓ Strict: -Wpedantic found" output.txt || { echo "ERROR: gfortran strict flag -Wpedantic not found"; exit 1; } +fi +echo "✓ Testing profile works" + +# Test 18: Individual features - debug only +echo "Test 18: Features per compiler - debug feature only" +rm -rf build +if "$fpm" run --features debug > output.txt 2>&1; then + echo "✓ Exit code 0 (success) as expected" +else + echo "ERROR: Expected exit code 0 but got non-zero exit code" + echo "=== Program output ===" + cat output.txt + echo "======================" + exit 1 +fi +grep -q "Features Per Compiler Demo" output.txt || { echo "ERROR: Features Per Compiler Demo not found"; exit 1; } +grep -q "✓ DEBUG: debug flags found" output.txt || { echo "ERROR: Debug feature not detected"; exit 1; } +grep -q "✓ All compiler flag checks PASSED" output.txt || { echo "ERROR: Expected all checks to pass"; exit 1; } +# Should NOT have release or fast flags +if grep -q "✓ RELEASE: -O flags found" output.txt; then + echo "ERROR: Release flags should not be present with debug only" + exit 1 +fi +echo "✓ Debug feature works" + +# Test 19: Individual features - release only +echo "Test 19: Features per compiler - release feature only" +rm -rf build +if "$fpm" run --features release > output.txt 2>&1; then + echo "✓ Exit code 0 (success) as expected" +else + echo "ERROR: Expected exit code 0 but got non-zero exit code" + echo "=== Program output ===" + cat output.txt + echo "======================" + exit 1 +fi +grep -q "Features Per Compiler Demo" output.txt || { echo "ERROR: Features Per Compiler Demo not found"; exit 1; } +grep -q "✓ RELEASE: -O flags found" output.txt || { echo "ERROR: Release feature not detected"; exit 1; } +grep -q "✓ All compiler flag checks PASSED" output.txt || { echo "ERROR: Expected all checks to pass"; exit 1; } +# Should NOT have debug flags +if grep -q "✓ DEBUG: debug flags found" output.txt; then + echo "ERROR: Debug flags should not be present with release only" + exit 1 +fi +echo "✓ Release feature works" + +# Test 20: No profile/features - baseline +echo "Test 20: Features per compiler - baseline (no profile)" +rm -rf build +if "$fpm" run > output.txt 2>&1; then + echo "✓ Exit code 0 (success) as expected" +else + echo "ERROR: Expected exit code 0 but got non-zero exit code" + echo "=== Program output ===" + cat output.txt + echo "======================" + exit 1 +fi +grep -q "Features Per Compiler Demo" output.txt || { echo "ERROR: Features Per Compiler Demo not found"; exit 1; } +grep -q "✓ All compiler flag checks PASSED" output.txt || { echo "ERROR: Expected all checks to pass"; exit 1; } +# Should NOT have any feature flags in baseline +if grep -q "✓ DEBUG: debug flags found" output.txt; then + echo "ERROR: Debug flags should not be present in baseline" + exit 1 +fi +if grep -q "✓ RELEASE: -O flags found" output.txt; then + echo "ERROR: Release flags should not be present in baseline" + exit 1 +fi +echo "✓ Baseline (no profile) works" + +# Cleanup +rm -rf build output.txt +popd + +echo "All FPM features tests passed!" diff --git a/example_packages/features_demo/app/debug_demo.f90 b/example_packages/features_demo/app/debug_demo.f90 new file mode 100644 index 0000000000..6b7a44c768 --- /dev/null +++ b/example_packages/features_demo/app/debug_demo.f90 @@ -0,0 +1,16 @@ +program debug_demo + use features_demo + implicit none + + write(*,*) 'Debug Demo Program' + write(*,*) '==================' + +#ifdef DEBUG + write(*,*) 'Debug mode: ON' +#else + write(*,*) 'Debug mode: OFF' +#endif + + call show_features() + +end program debug_demo diff --git a/example_packages/features_demo/app/main.f90 b/example_packages/features_demo/app/main.f90 new file mode 100644 index 0000000000..fcabe98cb6 --- /dev/null +++ b/example_packages/features_demo/app/main.f90 @@ -0,0 +1,12 @@ +program main + use features_demo + implicit none + + call show_features() + + write(*,*) '' + write(*,*) get_build_info() + write(*,*) '' + write(*,*) 'Demo completed successfully!' + +end program main \ No newline at end of file diff --git a/example_packages/features_demo/fpm.toml b/example_packages/features_demo/fpm.toml new file mode 100644 index 0000000000..2f03836dc3 --- /dev/null +++ b/example_packages/features_demo/fpm.toml @@ -0,0 +1,35 @@ +name = "features_demo" +version = "0.1.0" +license = "MIT" +description = "Demo package for FPM features functionality" + +[[executable]] +name = "features_demo" +source-dir = "app" +main = "main.f90" + +[features] +# Base debug feature +debug.flags = "-g" +debug.preprocess.cpp.macros = "DEBUG" + +# Release feature +release.flags = "-O3" +release.preprocess.cpp.macros = "RELEASE" + +# Compiler-specific features +debug.gfortran.flags = "-Wall -fcheck=bounds" +release.gfortran.flags = "-march=native" + +# Platform-specific features +linux.preprocess.cpp.macros = "LINUX_BUILD" + +# Parallel features +mpi.preprocess.cpp.macros = "USE_MPI" +mpi.dependencies.mpi = "*" +openmp.preprocess.cpp.macros = "USE_OPENMP" +openmp.dependencies.openmp = "*" + +[profiles] +development = ["debug"] +production = ["release", "openmp"] diff --git a/example_packages/features_demo/src/features_demo.f90 b/example_packages/features_demo/src/features_demo.f90 new file mode 100644 index 0000000000..86984ee56a --- /dev/null +++ b/example_packages/features_demo/src/features_demo.f90 @@ -0,0 +1,78 @@ +module features_demo + implicit none + private + public :: show_features, get_build_info + +contains + + !> Display which features are enabled + subroutine show_features() + write(*,*) 'FPM Features Demo' + write(*,*) '=================' + + ! Debug/Release flags +#ifdef DEBUG + write(*,*) '✓ DEBUG mode enabled' +#endif +#ifdef RELEASE + write(*,*) '✓ RELEASE mode enabled' +#endif + + ! Platform detection +#ifdef LINUX_BUILD + write(*,*) '✓ Linux platform detected' +#endif +#ifdef WINDOWS_BUILD + write(*,*) '✓ Windows platform detected' +#endif + + ! Parallel features +#ifdef USE_MPI + write(*,*) '✓ MPI support enabled' +#endif +#ifdef USE_OPENMP + write(*,*) '✓ OpenMP support enabled' +#endif + + ! Compiler info (if available) + write(*,*) 'Build configuration:' + call show_compiler_info() + + end subroutine show_features + + !> Show compiler information + subroutine show_compiler_info() +#ifdef __GFORTRAN__ + write(*,*) ' - Compiler: GNU Fortran' +#endif +#ifdef __INTEL_COMPILER + write(*,*) ' - Compiler: Intel Fortran' +#endif + end subroutine show_compiler_info + + !> Get build information as a string + function get_build_info() result(info) + character(len=200) :: info + + info = 'Features: ' + +#ifdef DEBUG + info = trim(info) // ' DEBUG' +#endif +#ifdef RELEASE + info = trim(info) // ' RELEASE' +#endif +#ifdef USE_MPI + info = trim(info) // ' MPI' +#endif +#ifdef USE_OPENMP + info = trim(info) // ' OPENMP' +#endif + + if (len_trim(info) == 10) then ! Only "Features: " + info = trim(info) // 'NONE' + end if + + end function get_build_info + +end module features_demo diff --git a/example_packages/features_per_compiler/.gitignore b/example_packages/features_per_compiler/.gitignore new file mode 100644 index 0000000000..8617481f53 --- /dev/null +++ b/example_packages/features_per_compiler/.gitignore @@ -0,0 +1 @@ +output.txt diff --git a/example_packages/features_per_compiler/app/main.f90 b/example_packages/features_per_compiler/app/main.f90 new file mode 100644 index 0000000000..5eaffcf746 --- /dev/null +++ b/example_packages/features_per_compiler/app/main.f90 @@ -0,0 +1,210 @@ +program main + use iso_fortran_env, only: compiler_options + implicit none + + character(len=:), allocatable :: options_str,detected_compiler + logical :: debug_active, release_active, verbose_active, fast_active, strict_active + logical :: all_checks_passed + integer :: failed_checks + + ! Get compiler flags used to build this file + allocate(options_str, source=compiler_options()) + + ! Display compiler information + print '(a)', '=================================' + print '(a)', 'Features Per Compiler Demo' + print '(a)', '=================================' + print '(a)', '' + + ! Detect compiler type using the function + allocate(detected_compiler, source=compiled_with()) + + print '(2a)', 'Detected compiler: ', detected_compiler + print '(a)', '' + print '(2a)', 'Compiler options: ', trim(options_str) + print '(a)', '' + + ! Check for feature flags + debug_active = index(options_str, ' -g ') > 0 + release_active = index(options_str, '-O3') > 0 + verbose_active = index(options_str, ' -v') > 0 .or. index(options_str, ' -v ') > 0 + fast_active = index(options_str, 'fast') > 0 + strict_active = index(options_str, '-std=f2018') > 0 .or. index(options_str, '-stand f18') > 0 + + ! Display active features + print '(a)', 'Active features detected:' + if (debug_active) print '(a)', ' ✓ DEBUG: debug flags found' + if (release_active) print '(a)', ' ✓ RELEASE: -O flags found' + if (verbose_active) print '(a)', ' ✓ VERBOSE: -v flag found' + if (fast_active) print '(a)', ' ✓ FAST: fast optimization flags found' + if (strict_active) print '(a)', ' ✓ STRICT: standard compliance flags found' + + print '(a)', '' + + ! Check compiler-specific flags and validate + failed_checks = check_compiler_flags(detected_compiler, options_str, debug_active, release_active, fast_active, strict_active) + + print '(a)', '' + + ! Determine overall result + all_checks_passed = (failed_checks == 0) + + if (all_checks_passed) then + print '(a)', '✓ All compiler flag checks PASSED' + print '(a)', '' + else + print '(a,i0,a)', '✗ ', failed_checks, ' compiler flag checks FAILED' + print '(a)', '' + end if + + ! Exit with appropriate code + stop merge(0,1,all_checks_passed) + +contains + + function check_compiler_flags(compiler, options, debug_on, release_on, fast_on, strict_on) result(failed_count) + character(len=*), intent(in) :: compiler, options + logical, intent(in) :: debug_on, release_on, fast_on, strict_on + integer :: failed_count + + failed_count = 0 + select case (compiler) + case ('gfortran') + failed_count = check_gfortran_flags(options, debug_on, release_on, fast_on, strict_on) + case ('ifort') + failed_count = check_ifort_flags(options, debug_on, release_on, fast_on, strict_on) + case ('ifx') + failed_count = check_ifx_flags(options, debug_on, release_on, fast_on, strict_on) + case default + print '(a)', 'Compiler-specific checks: Unknown compiler - only base flags checked' + end select + end function + + function check_flag(options, flag_name, feature_name, description) result(found) + character(len=*), intent(in) :: options, flag_name, feature_name, description + logical :: found + + found = index(options, flag_name) > 0 + if (found) then + print '(a,a,a,a,a)', ' ✓ ', feature_name, ': ', description, ' found' + else + print '(a,a,a,a,a)', ' ✗ ', feature_name, ': ', description, ' NOT found' + end if + end function + + function compiled_with() result(msg) + use iso_fortran_env, only: compiler_version + character(len=:), allocatable :: msg + character(len=:), allocatable :: version_str + + allocate(version_str, source=compiler_version()) + + if (index(version_str, 'GCC') > 0) then + msg = 'gfortran' + else if (index(version_str, 'Classic') > 0) then + msg = 'ifort' + else if (index(version_str, 'Intel') > 0) then + msg = 'ifx' + else + msg = 'any' + end if + end function + + function check_gfortran_flags(options, debug_on, release_on, fast_on, strict_on) result(failed_count) + character(len=*), intent(in) :: options + logical, intent(in) :: debug_on, release_on, fast_on, strict_on + integer :: failed_count + + failed_count = 0 + print '(a)', 'Compiler-specific flag checks (gfortran):' + + if (debug_on) then + if (.not. check_flag(options, '-Wall', 'Debug', '-Wall')) failed_count = failed_count + 1 + if (.not. check_flag(options, '-Wextra', 'Debug', '-Wextra')) failed_count = failed_count + 1 + if (.not. check_flag(options, '-fcheck=bounds', 'Debug', '-fcheck=bounds')) failed_count = failed_count + 1 + if (.not. check_flag(options, '-fbacktrace', 'Debug', '-fbacktrace')) failed_count = failed_count + 1 + end if + + if (release_on) then + ! Check for either -march or -mcpu (Apple Silicon uses -mcpu; -match=native may be re-resolved by gcc) + if (.not. (index(options, '-march') > 0 .or. index(options, '-mcpu') > 0)) then + print '(a)', ' ✗ Release: neither -march=native nor -mcpu found' + failed_count = failed_count + 1 + else + if (index(options, '-march=native') > 0) then + print '(a)', ' ✓ Release: -march found' + else + print '(a)', ' ✓ Release: -mcpu found' + end if + end if + if (.not. check_flag(options, '-funroll-loops', 'Release', '-funroll-loops')) failed_count = failed_count + 1 + end if + + if (fast_on) then + if (.not. check_flag(options, '-ffast', 'Fast', '-fast')) failed_count = failed_count + 1 + if (.not. check_flag(options, '-ffast-math', 'Fast', '-ffast-math')) failed_count = failed_count + 1 + end if + + if (strict_on) then + if (.not. check_flag(options, '-Wpedantic', 'Strict', '-Wpedantic')) failed_count = failed_count + 1 + if (.not. check_flag(options, '-Werror', 'Strict', '-Werror')) failed_count = failed_count + 1 + end if + end function + + function check_ifort_flags(options, debug_on, release_on, fast_on, strict_on) result(failed_count) + character(len=*), intent(in) :: options + logical, intent(in) :: debug_on, release_on, fast_on, strict_on + integer :: failed_count + + failed_count = 0 + print '(a)', 'Compiler-specific flag checks (ifort):' + + if (debug_on) then + if (.not. check_flag(options, '-warn all', 'Debug', '-warn all')) failed_count = failed_count + 1 + if (.not. check_flag(options, '-check bounds', 'Debug', '-check bounds')) failed_count = failed_count + 1 + if (.not. check_flag(options, '-traceback', 'Debug', '-traceback')) failed_count = failed_count + 1 + end if + + if (release_on) then + if (.not. check_flag(options, '-unroll', 'Release', '-unroll')) failed_count = failed_count + 1 + end if + + if (fast_on) then + if (.not. check_flag(options, '-fp-model', 'Fast', 'fast')) failed_count = failed_count + 1 + end if + + if (strict_on) then + if (.not. check_flag(options, '-stand f18', 'Strict', '-stand f18')) failed_count = failed_count + 1 + if (.not. check_flag(options, '-warn errors', 'Strict', '-warn errors')) failed_count = failed_count + 1 + end if + end function + + function check_ifx_flags(options, debug_on, release_on, fast_on, strict_on) result(failed_count) + character(len=*), intent(in) :: options + logical, intent(in) :: debug_on, release_on, fast_on, strict_on + integer :: failed_count + + failed_count = 0 + print '(a)', 'Compiler-specific flag checks (ifx):' + + if (debug_on) then + if (.not. check_flag(options, '-warn all', 'Debug', '-warn all')) failed_count = failed_count + 1 + if (.not. check_flag(options, '-check bounds', 'Debug', '-check bounds')) failed_count = failed_count + 1 + if (.not. check_flag(options, '-traceback', 'Debug', '-traceback')) failed_count = failed_count + 1 + end if + + if (release_on) then + if (.not. check_flag(options, '-unroll', 'Release', '-unroll')) failed_count = failed_count + 1 + end if + + if (fast_on) then + if (.not. check_flag(options, '-fp-model', 'Fast', 'fast')) failed_count = failed_count + 1 + end if + + if (strict_on) then + if (.not. check_flag(options, '-stand f18', 'Strict', '-stand f18')) failed_count = failed_count + 1 + if (.not. check_flag(options, '-warn errors', 'Strict', '-warn errors')) failed_count = failed_count + 1 + end if + end function + +end program main diff --git a/example_packages/features_per_compiler/fpm.toml b/example_packages/features_per_compiler/fpm.toml new file mode 100644 index 0000000000..168fa27f7b --- /dev/null +++ b/example_packages/features_per_compiler/fpm.toml @@ -0,0 +1,55 @@ +name = "features_per_compiler" +version = "0.1.0" +license = "MIT" +author = "Federico Perini" +maintainer = "federico.perini@gmail.com" +copyright = "Copyright 2025, Federico Perini" +description = "Demo package showcasing features with per-compiler flags and introspection" + +[build] +auto-executables=false + +[[executable]] +name = "features_per_compiler" +source-dir = "app" +main = "main.f90" + +[profiles] +# Development profile with debugging flags +development = ["debug", "verbose"] + +# Production profile with optimization flags +production = ["release", "fast"] + +# Testing profile with strict checking +testing = ["debug", "strict"] + +[features] +# Debug feature with base flags for all compilers, then per-compiler extensions +debug.flags = "-g" # Base debug flag for ALL compilers (applied first) +debug.gfortran.flags = "-Wall -Wextra -fcheck=bounds,do,mem,pointer -fbacktrace" +debug.ifort.flags = "-warn all -check bounds -traceback" # Unix/Linux/macOS Intel +debug.ifx.flags = "-warn all -check bounds -traceback" # Intel oneAPI +debug.preprocess.cpp.macros = "DEBUG" + +# Release feature with base optimization, then per-compiler extensions +release.flags = "-O3" # Base optimization for ALL compilers (applied first) +release.gfortran.flags = "-march=native -funroll-loops" +release.ifort.flags = "-unroll" # Unix/Linux/macOS Intel +release.ifx.flags = "-unroll" # Intel oneAPI + +# Verbose feature for enhanced diagnostics (applies to all compilers) +verbose.flags = "-v" + +# Fast feature with base optimization, then per-compiler extensions +fast.flags = "-O3" # Base fast optimization for ALL compilers (applied first) +fast.gfortran.flags = "-Ofast -ffast-math" +fast.ifort.flags = "-fp-model fast" +fast.ifx.flags = "-fp-model fast" + +# Strict feature with base standard compliance, then per-compiler extensions +strict.flags = "-std=f2018" # Base standard compliance for ALL compilers (applied first) +strict.gfortran.flags = "-Wpedantic -Werror" +strict.ifort.flags = "-stand f18 -warn errors" +strict.ifx.flags = "-stand f18 -warn errors" + diff --git a/example_packages/features_with_dependency/app/main.f90 b/example_packages/features_with_dependency/app/main.f90 new file mode 100644 index 0000000000..d0039204e2 --- /dev/null +++ b/example_packages/features_with_dependency/app/main.f90 @@ -0,0 +1,13 @@ +program features_with_dependency_demo + use features_with_dependency, only: show_features + implicit none + + print *, "=== Features with Dependency Demo ===" + print *, "" + + call show_features() + + print *, "" + print *, "This demonstrates feature propagation to dependencies." + +end program features_with_dependency_demo diff --git a/example_packages/features_with_dependency/fpm.toml b/example_packages/features_with_dependency/fpm.toml new file mode 100644 index 0000000000..055c6dbffd --- /dev/null +++ b/example_packages/features_with_dependency/fpm.toml @@ -0,0 +1,40 @@ +name = "features_with_dependency" +version = "0.1.0" +license = "MIT" +description = "Demo package testing dependency features" + +[[executable]] +name = "features_with_dependency" +source-dir = "app" +main = "main.f90" + +[dependencies] +# Base dependencies (none for this demo - features will add them) + +[features] +# Feature that enables debug mode in the dependency +with_feat_debug.dependencies.features_demo = { path = "../features_demo", features = ["debug"] } +with_feat_debug.preprocess.cpp.macros = ["WITH_DEMO","WITH_DEBUG_DEPENDENCY"] + +# Feature that enables release mode in the dependency +with_feat_release.dependencies.features_demo = { path = "../features_demo", features = ["release"] } +with_feat_release.preprocess.cpp.macros = ["WITH_DEMO","WITH_RELEASE_DEPENDENCY"] + +# Feature that enables multiple dependency features +with_feat_multi.dependencies.features_demo = { path = "../features_demo", features = ["debug", "mpi"] } +with_feat_multi.preprocess.cpp.macros = ["WITH_DEMO","WITH_MULTI_DEPENDENCY"] + +# Feature for platform-specific dependency features +linux_specific.linux.dependencies.features_demo = { path = "../features_demo", features = ["linux"] } +linux_specific.preprocess.cpp.macros = ["WITH_DEMO","LINUX_FEATURES"] + +# Feature combining compiler and dependency features +gfortran_optimized.gfortran.dependencies.features_demo = { path = "../features_demo", features = ["release", "gfortran"] } +gfortran_optimized.gfortran.flags = "-O3 -march=native" +gfortran_optimized.preprocess.cpp.macros = ["WITH_DEMO"] + + +[profiles] +debug_dep = ["with_feat_debug"] +release_dep = ["with_feat_release"] +full_test = ["with_feat_multi", "linux_specific"] diff --git a/example_packages/features_with_dependency/src/features_with_dependency.f90 b/example_packages/features_with_dependency/src/features_with_dependency.f90 new file mode 100644 index 0000000000..b8eccdf7a3 --- /dev/null +++ b/example_packages/features_with_dependency/src/features_with_dependency.f90 @@ -0,0 +1,45 @@ +module features_with_dependency +#ifdef WITH_DEMO + use features_demo, only: show_demo_features => show_features +#endif + implicit none + private + public :: show_features + +contains + + subroutine show_features() + print *, "Local package features:" + +#ifdef WITH_DEBUG_DEPENDENCY + print *, "✓ WITH_DEBUG_DEPENDENCY - dependency built with debug features" +#endif + +#ifdef WITH_RELEASE_DEPENDENCY + print *, "✓ WITH_RELEASE_DEPENDENCY - dependency built with release features" +#endif + +#ifdef WITH_MULTI_DEPENDENCY + print *, "✓ WITH_MULTI_DEPENDENCY - dependency built with debug+mpi features" +#endif + +#ifdef LINUX_FEATURES + print *, "✓ LINUX_FEATURES - Linux-specific dependency features" +#endif + + ! If no local features are active +#if !defined(WITH_DEBUG_DEPENDENCY) && !defined(WITH_RELEASE_DEPENDENCY) && !defined(WITH_MULTI_DEPENDENCY) && !defined(LINUX_FEATURES) + print *, " NONE - no local features active" +#endif + + print *, "" +#ifdef WITH_DEMO + print *, "Dependency (features_demo) status:" + call show_demo_features() ! Call features_demo's show_features +#else + print *, "Dependency (features_demo) is not attached" +#endif + + end subroutine show_features + +end module features_with_dependency diff --git a/example_packages/test_duplicates/app/main.f90 b/example_packages/test_duplicates/app/main.f90 new file mode 100644 index 0000000000..d7544d4ec3 --- /dev/null +++ b/example_packages/test_duplicates/app/main.f90 @@ -0,0 +1,3 @@ +program test_program + print *, "Hello from test program" +end program test_program \ No newline at end of file diff --git a/example_packages/test_duplicates/fpm.toml b/example_packages/test_duplicates/fpm.toml new file mode 100644 index 0000000000..69cbb4a6fc --- /dev/null +++ b/example_packages/test_duplicates/fpm.toml @@ -0,0 +1,21 @@ +name = "test_duplicates" +version = "0.1.0" + +[[executable]] +name = "test_program" +source-dir = "app" +main = "main.f90" + +[features] +# This should cause a duplicate executable error when features are combined +feature1.executable.name = "my_exe" +feature1.executable.source-dir = "app1" +feature1.executable.main = "main1.f90" + +feature2.executable.name = "my_exe" # Same name - should cause duplicate error +feature2.executable.source-dir = "app2" +feature2.executable.main = "main2.f90" + +[profiles] +# This profile combines features with duplicate executables +combined = ["feature1", "feature2"] \ No newline at end of file diff --git a/src/fpm.f90 b/src/fpm.f90 index f8ec95921d..b7cc9e1e13 100644 --- a/src/fpm.f90 +++ b/src/fpm.f90 @@ -12,7 +12,9 @@ module fpm use fpm_model, only: fpm_model_t, srcfile_t, show_model, & FPM_SCOPE_UNKNOWN, FPM_SCOPE_LIB, FPM_SCOPE_DEP, & FPM_SCOPE_APP, FPM_SCOPE_EXAMPLE, FPM_SCOPE_TEST -use fpm_compiler, only: new_compiler, new_archiver, set_cpp_preprocessor_flags +use fpm_compiler, only: new_compiler, new_archiver, set_cpp_preprocessor_flags, & + id_intel_classic_nix,id_intel_classic_mac,id_intel_llvm_nix, & + id_intel_llvm_unknown use fpm_sources, only: add_executable_sources, add_sources_from_dir @@ -33,7 +35,7 @@ module fpm implicit none private public :: cmd_build, cmd_run, cmd_clean -public :: build_model, check_modules_for_duplicates +public :: build_model, check_modules_for_duplicates, new_compiler_flags contains @@ -78,14 +80,17 @@ subroutine build_model(model, settings, package_config, error) ! Extract the target platform for this build target_platform = model%target_platform() - - call new_compiler_flags(model,settings) + model%build_dir = settings%build_dir model%build_prefix = join_path(settings%build_dir, basename(model%compiler%fc)) - model%include_tests = settings%build_tests - + model%include_tests = settings%build_tests + ! Extract the current package configuration request - package = package_config%export_config(target_platform) + package = package_config%export_config(target_platform,settings%features,settings%profile,error) + if (allocated(error)) return + + ! Initialize compiler flags using the feature-enabled package configuration + call new_compiler_flags(model, settings, package) ! Resolve meta-dependencies into the package and the model call resolve_metapackages(model,package,settings,error) @@ -114,7 +119,11 @@ subroutine build_model(model, settings, package_config, error) end if allocate(model%packages(model%deps%ndep)) - has_cpp = .false. + + ! The current configuration may not have preprocessing, but some of its features may. + ! This means there will be directives that need to be considered even if not currently + ! active. Turn preprocessing on even in this case + has_cpp = package_config%has_cpp() .or. package%has_cpp() do i = 1, model%deps%ndep associate(dep => model%deps%dep(i)) @@ -131,7 +140,9 @@ subroutine build_model(model, settings, package_config, error) if (allocated(error)) exit ! Adapt it to the current profile/platform - dependency = dependency_config%export_config(target_platform) + dependency = dependency_config%export_config(target_platform, & + dep%features,error=error) + if (allocated(error)) exit manifest => dependency end if @@ -307,31 +318,68 @@ subroutine build_model(model, settings, package_config, error) end if end subroutine build_model -!> Initialize model compiler flags -subroutine new_compiler_flags(model,settings) +!> Helper: safely get string from either CLI or package, with fallback +pure function assemble_flags(cli_flag, package_flag, fallback) result(flags) + character(len=*), optional, intent(in) :: cli_flag, package_flag, fallback + character(len=:), allocatable :: flags + + allocate(character(len=0) :: flags) + + if (present(cli_flag)) flags = flags // ' ' // trim(cli_flag) + if (present(package_flag)) flags = flags // ' ' // trim(package_flag) + if (present(fallback)) flags = flags // ' ' // trim(fallback) + +end function assemble_flags + +!> Initialize model compiler flags from CLI settings and package configuration +subroutine new_compiler_flags(model, settings, package) type(fpm_model_t), intent(inout) :: model type(fpm_build_settings), intent(in) :: settings + type(package_config_t), intent(in) :: package - character(len=:), allocatable :: flags, cflags, cxxflags, ldflags - - if (settings%flag == '') then - flags = model%compiler%get_default_flags(settings%profile == "release") - else - flags = settings%flag - select case(settings%profile) - case("release", "debug") - flags = flags // model%compiler%get_default_flags(settings%profile == "release") - end select + logical :: release_request, debug_request, need_defaults + character(len=:), allocatable :: fallback + + ! Default: "debug" if not requested + release_request = .false. + debug_request = .not.allocated(settings%profile) + if (allocated(settings%profile)) release_request = settings%profile == "release" + if (allocated(settings%profile)) debug_request = settings%profile == "debug" + + need_defaults = release_request .or. debug_request + + ! Backward-compatible: if debug/release requested, but a user-defined profile is not defined, + ! apply fpm compiler defaults + if (need_defaults) then + + need_defaults = (release_request .and. package%find_profile("release")<=0) & + .or. (debug_request .and. package%find_profile("debug")<=0) + end if + + ! Fix: Always include compiler default flags for Intel ifx -fPIC issue + if (need_defaults) then + + fallback = model%compiler%get_default_flags(release_request) + + elseif (any(model%compiler%id==[id_intel_classic_mac, & + id_intel_classic_nix, & + id_intel_llvm_nix, & + id_intel_llvm_unknown])) then - cflags = trim(settings%cflag) - cxxflags = trim(settings%cxxflag) - ldflags = trim(settings%ldflag) - - model%fortran_compile_flags = flags - model%c_compile_flags = cflags - model%cxx_compile_flags = cxxflags - model%link_flags = ldflags + ! Intel compilers need -fPIC for shared libraries (except Windows) + fallback = " -fPIC" + + else + + if (allocated(fallback)) deallocate(fallback) ! trigger .not.present + + endif + + model%fortran_compile_flags = assemble_flags(settings%flag, package%flags, fallback) + model%c_compile_flags = assemble_flags(settings%cflag, package%c_flags) + model%cxx_compile_flags = assemble_flags(settings%cxxflag, package%cxx_flags) + model%link_flags = assemble_flags(settings%ldflag, package%link_time_flags) end subroutine new_compiler_flags diff --git a/src/fpm/dependency.f90 b/src/fpm/dependency.f90 index a136fb0a7a..dee4fd750d 100644 --- a/src/fpm/dependency.f90 +++ b/src/fpm/dependency.f90 @@ -96,7 +96,7 @@ module fpm_dependency logical :: update = .false. !> Dependency was loaded from a cache logical :: cached = .false. - !> Package dependencies of this node + !> Internal: package dependencies of this node type(string_t), allocatable :: package_dep(:) contains diff --git a/src/fpm/installer.f90 b/src/fpm/installer.f90 index 704c10a243..1a8a98ee94 100644 --- a/src/fpm/installer.f90 +++ b/src/fpm/installer.f90 @@ -208,6 +208,7 @@ subroutine install_executable(self, executable, error) end if call self%install(executable, self%bindir, error) + if (allocated(error)) return ! on MacOS, add two relative paths for search of dynamic library dependencies: add_rpath: if (self%os==OS_MACOS) then diff --git a/src/fpm/manifest/dependency.f90 b/src/fpm/manifest/dependency.f90 index c2bfa619a0..b03d545c4d 100644 --- a/src/fpm/manifest/dependency.f90 +++ b/src/fpm/manifest/dependency.f90 @@ -28,7 +28,7 @@ module fpm_manifest_dependency & git_target_revision, git_target_default, git_matches_manifest use tomlf, only: toml_table, toml_key, toml_stat use fpm_toml, only: get_value, check_keys, serializable_t, add_table, & - & set_value, set_string + & set_value, set_string, get_list, set_list use fpm_filesystem, only: windows_path, join_path use fpm_environment, only: get_os_type, OS_WINDOWS use fpm_manifest_metapackages, only: metapackage_config_t, is_meta_package, new_meta_config, & @@ -62,6 +62,9 @@ module fpm_manifest_dependency !> Requested macros for the dependency type(preprocess_config_t), allocatable :: preprocess(:) + + !> Requested features for the dependency + type(string_t), allocatable :: features(:) !> Git descriptor type(git_target_t), allocatable :: git @@ -128,6 +131,10 @@ subroutine new_dependency(self, table, root, error) call new_preprocessors(self%preprocess, child, error) if (allocated(error)) return endif + + !> Get optional features list + call get_list(table, "features", self%features, error) + if (allocated(error)) return call get_value(table, "path", uri) if (allocated(uri)) then @@ -176,6 +183,7 @@ subroutine check(table, error) type(error_t), allocatable, intent(out) :: error character(len=:), allocatable :: name + type(string_t), allocatable :: string_list(:) type(toml_key), allocatable :: list(:) type(toml_table), pointer :: child @@ -188,7 +196,8 @@ subroutine check(table, error) "tag", & "branch", & "rev", & - "preprocess" & + "preprocess", & + "features" & & ] call table%get_key(name) @@ -243,7 +252,7 @@ subroutine check(table, error) end if end if - + end subroutine check !> Construct new dependency array from a TOML data structure @@ -341,7 +350,7 @@ subroutine info(self, unit, verbosity) !> Verbosity of the printout integer, intent(in), optional :: verbosity - integer :: pr + integer :: pr, ilink character(len=*), parameter :: fmt = '("#", 1x, a, t30, a)' if (present(verbosity)) then @@ -365,6 +374,13 @@ subroutine info(self, unit, verbosity) write (unit, fmt) "- path", self%path end if + if (allocated(self%features)) then + write(unit, fmt) " - features" + do ilink = 1, size(self%features) + write(unit, fmt) " - " // self%features(ilink)%s + end do + end if + end subroutine info !> Check if two dependency configurations are different @@ -401,6 +417,7 @@ elemental subroutine dependency_destroy(self) if (allocated(self%namespace)) deallocate(self%namespace) if (allocated(self%requested_version)) deallocate(self%requested_version) if (allocated(self%git)) deallocate(self%git) + if (allocated(self%features)) deallocate(self%features) end subroutine dependency_destroy @@ -430,6 +447,10 @@ logical function dependency_is_same(this,that) if (allocated(this%requested_version)) then if (.not.(this%requested_version==other%requested_version)) return endif + if (allocated(this%features).neqv.allocated(other%features)) return + if (allocated(this%features)) then + if (.not.(this%features==other%features)) return + endif if ((allocated(this%git).neqv.allocated(other%git))) return if (allocated(this%git)) then @@ -471,6 +492,8 @@ subroutine dump_to_toml(self, table, error) call set_string(table, "requested_version", self%requested_version%s(), error, 'dependency_config_t') if (allocated(error)) return endif + call set_list(table, "features", self%features, error) + if (allocated(error)) return if (allocated(self%git)) then call add_table(table, "git", ptr, error) @@ -513,6 +536,8 @@ subroutine load_from_toml(self, table, error) return endif end if + call get_list(table, "features", self%features, error) + if (allocated(error)) return call table%get_keys(list) add_git: do ii = 1, size(list) diff --git a/src/fpm/manifest/feature.f90 b/src/fpm/manifest/feature.f90 index d9c878eb16..5d3f65a08b 100644 --- a/src/fpm/manifest/feature.f90 +++ b/src/fpm/manifest/feature.f90 @@ -100,7 +100,7 @@ module fpm_manifest_feature character(len=:), allocatable :: cxx_flags character(len=:), allocatable :: link_time_flags - !> Feature dependencies + !> Feature dependencies (not active yet) type(string_t), allocatable :: requires_features(:) !> Is this feature enabled by default @@ -116,6 +116,9 @@ module fpm_manifest_feature !> Get manifest name procedure :: manifest_name + + !> Check if there is a cpp configuration + procedure :: has_cpp !> Serialization interface procedure :: serializable_is_same => feature_is_same @@ -1193,5 +1196,21 @@ function manifest_name(self) result(name) end if end function manifest_name + + !> Check if there is a CPP preprocessor configuration + elemental logical function has_cpp(self) + class(feature_config_t), intent(in) :: self + + integer :: i + + has_cpp = .false. + if (.not.allocated(self%preprocess)) return + + do i=1,size(self%preprocess) + has_cpp = self%preprocess(i)%is_cpp() + if (has_cpp) return + end do + + end function has_cpp end module fpm_manifest_feature diff --git a/src/fpm/manifest/feature_collection.f90 b/src/fpm/manifest/feature_collection.f90 index 8069b54e49..393e6a0739 100644 --- a/src/fpm/manifest/feature_collection.f90 +++ b/src/fpm/manifest/feature_collection.f90 @@ -24,7 +24,8 @@ module fpm_manifest_feature_collection private public :: new_collections, get_default_features, & - get_default_features_as_features, default_debug_feature, default_release_feature + default_debug_feature, default_release_feature, & + add_default_features, collection_from_feature !> Feature configuration data type, public, extends(serializable_t) :: feature_collection_t @@ -44,9 +45,16 @@ module fpm_manifest_feature_collection procedure :: push_variant procedure :: extract_for_target procedure :: check => check_collection + procedure :: merge_into_package + procedure :: has_cpp end type feature_collection_t + !> Interface for feature_collection_t constructor + interface feature_collection_t + module procedure collection_from_feature + end interface feature_collection_t + contains !> Equality (semantic): base and variants (size + element-wise) @@ -301,7 +309,17 @@ recursive subroutine traverse_feature_table(collection, table, feature_name, & ! Check if this key is an OS name os_type = match_os_type(keys(i)%key) if (os_type /= OS_UNKNOWN) then - ! This is an OS constraint - get subtable and recurse + ! This is an OS constraint + + ! Check for chained OS commands (e.g., feature.windows.linux) + if (constraint%os_type /= OS_ALL) then + call fatal_error(error, "Cannot chain OS constraints: '" // & + constraint%os_name() // "." // keys(i)%key // & + "' - OS was already specified") + return + end if + + ! Get subtable and recurse call get_value(table, keys(i)%key, subtable, stat=stat) if (stat == toml_stat%success) then platform = platform_config_t(constraint%compiler,os_type) @@ -315,6 +333,15 @@ recursive subroutine traverse_feature_table(collection, table, feature_name, & ! Check if this key is a compiler name compiler_type = match_compiler_type(keys(i)%key) if (compiler_type /= id_unknown) then + + ! Check for chained compiler commands (e.g., feature.gfortran.ifort) + if (constraint%compiler /= id_all) then + call fatal_error(error, "Cannot chain compiler constraints: '" // & + constraint%compiler_name() // "." // keys(i)%key // & + "' - compiler was already specified") + return + end if + ! This is a compiler constraint - get subtable and recurse call get_value(table, keys(i)%key, subtable, stat=stat) if (stat == toml_stat%success) then @@ -430,11 +457,21 @@ subroutine merge_feature_configs(target, source, error) end if ! ADDITIVE: Array properties - append source to target - call merge_executable_arrays(target%executable, source%executable) - call merge_dependency_arrays(target%dependency, source%dependency) - call merge_dependency_arrays(target%dev_dependency, source%dev_dependency) - call merge_example_arrays(target%example, source%example) - call merge_test_arrays(target%test, source%test) + call merge_executable_arrays(target%executable, source%executable, error) + if (allocated(error)) return + + call merge_dependency_arrays(target%dependency, source%dependency, error) + if (allocated(error)) return + + call merge_dependency_arrays(target%dev_dependency, source%dev_dependency, error) + if (allocated(error)) return + + call merge_example_arrays(target%example, source%example, error) + if (allocated(error)) return + + call merge_test_arrays(target%test, source%test, error) + if (allocated(error)) return + call merge_preprocess_arrays(target%preprocess, source%preprocess) call merge_string_arrays(target%requires_features, source%requires_features) @@ -443,19 +480,36 @@ subroutine merge_feature_configs(target, source, error) end subroutine merge_feature_configs - !> Merge executable arrays by appending source to target - subroutine merge_executable_arrays(target, source) + !> Merge executable arrays by appending source to target, checking for duplicates + subroutine merge_executable_arrays(target, source, error) type(executable_config_t), allocatable, intent(inout) :: target(:) type(executable_config_t), allocatable, intent(in) :: source(:) + type(error_t), allocatable, intent(out) :: error type(executable_config_t), allocatable :: temp(:) - integer :: target_size, source_size + integer :: target_size, source_size, i, j if (.not. allocated(source)) return source_size = size(source) if (source_size == 0) return + ! Check for duplicates between source and target + if (allocated(target)) then + target_size = size(target) + do i = 1, source_size + do j = 1, target_size + if (allocated(source(i)%name) .and. allocated(target(j)%name)) then + if (source(i)%name == target(j)%name) then + call fatal_error(error, "Duplicate executable '"//source(i)%name//"' found. " // & + "Multiple definitions of the same executable are not currently allowed.") + return + end if + end if + end do + end do + end if + if (.not. allocated(target)) then allocate(target(source_size), source=source) else @@ -468,19 +522,36 @@ subroutine merge_executable_arrays(target, source) end subroutine merge_executable_arrays - !> Merge dependency arrays by appending source to target - subroutine merge_dependency_arrays(target, source) + !> Merge dependency arrays by appending source to target, checking for duplicates + subroutine merge_dependency_arrays(target, source, error) type(dependency_config_t), allocatable, intent(inout) :: target(:) type(dependency_config_t), allocatable, intent(in) :: source(:) + type(error_t), allocatable, intent(out) :: error type(dependency_config_t), allocatable :: temp(:) - integer :: target_size, source_size + integer :: target_size, source_size, i, j if (.not. allocated(source)) return source_size = size(source) if (source_size == 0) return + ! Check for duplicates between source and target + if (allocated(target)) then + target_size = size(target) + do i = 1, source_size + do j = 1, target_size + if (allocated(source(i)%name) .and. allocated(target(j)%name)) then + if (source(i)%name == target(j)%name) then + call fatal_error(error, "Duplicate dependency '"//source(i)%name//"' found. " // & + "Multiple definitions of the same dependency are not currently allowed.") + return + end if + end if + end do + end do + end if + if (.not. allocated(target)) then allocate(target(source_size), source=source) else @@ -507,19 +578,36 @@ subroutine merge_string_additive(target, source) end if end subroutine merge_string_additive - !> Merge example arrays by appending source to target - subroutine merge_example_arrays(target, source) + !> Merge example arrays by appending source to target, checking for duplicates + subroutine merge_example_arrays(target, source, error) type(example_config_t), allocatable, intent(inout) :: target(:) type(example_config_t), allocatable, intent(in) :: source(:) + type(error_t), allocatable, intent(out) :: error type(example_config_t), allocatable :: temp(:) - integer :: target_size, source_size + integer :: target_size, source_size, i, j if (.not. allocated(source)) return source_size = size(source) if (source_size == 0) return + ! Check for duplicates between source and target + if (allocated(target)) then + target_size = size(target) + do i = 1, source_size + do j = 1, target_size + if (allocated(source(i)%name) .and. allocated(target(j)%name)) then + if (source(i)%name == target(j)%name) then + call fatal_error(error, "Duplicate example '"//source(i)%name//"' found. " // & + "Multiple definitions of the same example are not currently allowed.") + return + end if + end if + end do + end do + end if + if (.not. allocated(target)) then allocate(target(source_size), source=source) else @@ -531,19 +619,36 @@ subroutine merge_example_arrays(target, source) end if end subroutine merge_example_arrays - !> Merge test arrays by appending source to target - subroutine merge_test_arrays(target, source) + !> Merge test arrays by appending source to target, checking for duplicates + subroutine merge_test_arrays(target, source, error) type(test_config_t), allocatable, intent(inout) :: target(:) type(test_config_t), allocatable, intent(in) :: source(:) + type(error_t), allocatable, intent(out) :: error type(test_config_t), allocatable :: temp(:) - integer :: target_size, source_size + integer :: target_size, source_size, i, j if (.not. allocated(source)) return source_size = size(source) if (source_size == 0) return + ! Check for duplicates between source and target + if (allocated(target)) then + target_size = size(target) + do i = 1, source_size + do j = 1, target_size + if (allocated(source(i)%name) .and. allocated(target(j)%name)) then + if (source(i)%name == target(j)%name) then + call fatal_error(error, "Duplicate test '"//source(i)%name//"' found. " // & + "Multiple definitions of the same test are not currently allowed.") + return + end if + end if + end do + end do + end if + if (.not. allocated(target)) then allocate(target(source_size)) target = source @@ -556,13 +661,15 @@ subroutine merge_test_arrays(target, source) end if end subroutine merge_test_arrays - !> Merge preprocess arrays by appending source to target + !> Merge preprocess arrays by merging configurations for same preprocessor names + !> and appending new ones subroutine merge_preprocess_arrays(target, source) type(preprocess_config_t), allocatable, intent(inout) :: target(:) type(preprocess_config_t), allocatable, intent(in) :: source(:) type(preprocess_config_t), allocatable :: temp(:) - integer :: target_size, source_size + integer :: target_size, source_size, i, j, new_count + integer, allocatable :: source_to_target_map(:) ! Maps source index to target index (0 = new) if (.not. allocated(source)) return @@ -570,16 +677,70 @@ subroutine merge_preprocess_arrays(target, source) if (source_size == 0) return if (.not. allocated(target)) then - allocate(target(source_size)) - target = source - else - target_size = size(target) - allocate(temp(target_size + source_size)) + allocate(target(source_size), source=source) + return + end if + + target_size = size(target) + + ! Create mapping arrays in a single pass + allocate(source_to_target_map(source_size), source=0) + + ! Single loop to build the mapping + do i = 1, source_size + if (allocated(source(i)%name)) then + do j = 1, target_size + if (allocated(target(j)%name)) then + if (target(j)%name == source(i)%name) then + source_to_target_map(i) = j + exit + end if + end if + end do + end if + end do + + ! Merge overlapping configurations + do i = 1, source_size + j = source_to_target_map(i) + if (j==0) cycle ! new config + call merge_preprocessor_config(target(j), source(i)) + end do + + ! Count and add new preprocessors + new_count = count(source_to_target_map==0) + if (new_count > 0) then + allocate(temp(target_size + new_count)) temp(1:target_size) = target - temp(target_size+1:target_size+source_size) = source + + ! Add new preprocessors in a single pass + j = target_size + do i = 1, source_size + if (source_to_target_map(i)==0) then + j = j + 1 + temp(j) = source(i) + end if + end do + call move_alloc(temp, target) end if end subroutine merge_preprocess_arrays + + !> Helper to merge two preprocessor configurations with the same name + subroutine merge_preprocessor_config(target, source) + type(preprocess_config_t), intent(inout) :: target + type(preprocess_config_t), intent(in) :: source + + ! Merge suffixes arrays + call merge_string_arrays(target%suffixes, source%suffixes) + + ! Merge directories arrays + call merge_string_arrays(target%directories, source%directories) + + ! Merge macros arrays + call merge_string_arrays(target%macros, source%macros) + + end subroutine merge_preprocessor_config !> Merge string arrays by appending source to target subroutine merge_string_arrays(target, source) @@ -614,6 +775,7 @@ subroutine merge_metapackages_additive(target, source) ! OR logic: if either requests a metapackage, turn it on if (source%on) then target%on = .true. + target%name = source%name ! Use source version if target doesn't have one if (allocated(source%version) .and. .not. allocated(target%version)) then target%version = source%version @@ -748,52 +910,6 @@ subroutine get_default_features(collections, error) end subroutine get_default_features - !> Convert feature collections to individual features (for backward compatibility) - subroutine get_default_features_as_features(features, error) - - !> Features array to populate (backward compatible) - type(feature_config_t), allocatable, intent(out) :: features(:) - - !> Error handling - type(error_t), allocatable, intent(out) :: error - - type(feature_collection_t), allocatable :: collections(:) - integer :: total_features, ifeature, icol, ivar - - ! Get the feature collections - call get_default_features(collections, error) - if (allocated(error)) return - - ! Count total features needed - total_features = 0 - do icol = 1, size(collections) - total_features = total_features + 1 ! base feature - if (allocated(collections(icol)%variants)) then - total_features = total_features + size(collections(icol)%variants) - end if - end do - - ! Allocate features array - allocate(features(total_features)) - - ! Copy features from collections - ifeature = 1 - do icol = 1, size(collections) - ! Add base feature - features(ifeature) = collections(icol)%base - ifeature = ifeature + 1 - - ! Add variants - if (allocated(collections(icol)%variants)) then - do ivar = 1, size(collections(icol)%variants) - features(ifeature) = collections(icol)%variants(ivar) - ifeature = ifeature + 1 - end do - end if - end do - - end subroutine get_default_features_as_features - !> Helper to create a feature variant function default_variant(name, compiler_id, os_type, flags) result(feature) character(len=*), intent(in) :: name @@ -1026,16 +1142,42 @@ subroutine simulate_merge(target, source) target%library = source%library end if end subroutine simulate_merge + + !> Merge a feature configuration into an existing global package + subroutine merge_into_package(self, package, target, error) + class(feature_collection_t), intent(in) :: self + + class(feature_config_t), intent(inout) :: package + + type(platform_config_t), intent(in) :: target + + type(error_t), allocatable, intent(out) :: error + + type(feature_config_t) :: feature + + ! Extract the feature configuration for the target platform + feature = self%extract_for_target(target, error) + if (allocated(error)) return + + print *, 'extract for target: flags=',feature%flags + print *, 'extract for target: link=',feature%link_time_flags + + ! Merge the extracted feature into the package + call merge_feature_configs(package, feature, error) + print *, 'merged for target: flags=',package%flags + print *, 'merged for target: link=',package%link_time_flags + if (allocated(error)) return + + end subroutine merge_into_package !> Extract a merged feature configuration for the given target platform - function extract_for_target(self, target) result(feature) + type(feature_config_t) function extract_for_target(self, target, error) result(feature) class(feature_collection_t), intent(in) :: self type(platform_config_t), intent(in) :: target - type(feature_config_t) :: feature + type(error_t), allocatable, intent(out) :: error integer :: i - type(error_t), allocatable :: error - + ! Start with base feature as foundation feature = self%base @@ -1045,14 +1187,118 @@ function extract_for_target(self, target) result(feature) if (self%variants(i)%platform%matches(target)) then ! Merge this variant into the feature call merge_feature_configs(feature, self%variants(i), error) - if (allocated(error)) then - ! If merge fails, just continue with what we have - deallocate(error) - end if + if (allocated(error)) return end if end do end if end function extract_for_target + + !> Add default features to existing features array if they don't already exist + subroutine add_default_features(features, error) + + !> Instance of the feature collections array (will be resized) + type(feature_collection_t), allocatable, intent(inout) :: features(:) + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(feature_collection_t), allocatable :: temp_features(:) + type(feature_collection_t), allocatable :: default_features(:) + logical :: debug_exists, release_exists + integer :: i, current_size, new_size + + ! Get default features + call get_default_features(default_features, error) + if (allocated(error)) return + + ! Check if debug and release features already exist + debug_exists = .false. + release_exists = .false. + + if (allocated(features)) then + do i = 1, size(features) + if (allocated(features(i)%base%name)) then + if (features(i)%base%name == "debug") debug_exists = .true. + if (features(i)%base%name == "release") release_exists = .true. + end if + end do + current_size = size(features) + else + current_size = 0 + end if + + ! Calculate how many features to add + new_size = current_size + if (.not. debug_exists) new_size = new_size + 1 + if (.not. release_exists) new_size = new_size + 1 + + ! If nothing to add, return + if (new_size == current_size) return + + ! Create new array with existing + missing defaults + allocate(temp_features(new_size)) + + ! Copy existing features + if (current_size > 0) then + temp_features(1:current_size) = features(1:current_size) + end if + + ! Add missing defaults + i = current_size + if (.not. debug_exists) then + i = i + 1 + temp_features(i) = default_features(1) ! debug feature + end if + if (.not. release_exists) then + i = i + 1 + temp_features(i) = default_features(2) ! release feature + end if + + ! Replace the features array + call move_alloc(temp_features, features) + + end subroutine add_default_features + + + !> Create a feature collection from a single feature_config_t + !> The feature becomes the base configuration for all OS/compiler combinations + type(feature_collection_t) function collection_from_feature(self) result(collection) + + !> Feature configuration to convert + class(feature_config_t), intent(in) :: self + + ! Copy the feature into the base configuration + collection%base = self + + ! Set platform to all OS and all compilers for the base + collection%base%platform%os_type = OS_ALL + collection%base%platform%compiler = id_all + + ! Copy the name if available + if (allocated(self%name)) collection%base%name = self%name + + ! No variants initially - just the base configuration + ! (variants can be added later if needed) + + end function collection_from_feature + + + !> Check if there is a CPP preprocessor configuration + elemental logical function has_cpp(self) + class(feature_collection_t), intent(in) :: self + + integer :: i + + has_cpp = self%base%has_cpp() + if (has_cpp) return + if (.not.allocated(self%variants)) return + + do i=1,size(self%variants) + has_cpp = self%variants(i)%has_cpp() + if (has_cpp) return + end do + + end function has_cpp end module fpm_manifest_feature_collection diff --git a/src/fpm/manifest/meta.f90 b/src/fpm/manifest/meta.f90 index 50d8d667a3..51b972f45f 100644 --- a/src/fpm/manifest/meta.f90 +++ b/src/fpm/manifest/meta.f90 @@ -67,7 +67,10 @@ module fpm_manifest_metapackages contains procedure :: get_requests + + ! Cleanup configuration; assert package names final :: meta_config_final + procedure :: reset => meta_config_reset procedure :: serializable_is_same => meta_config_same procedure :: dump_to_toml => meta_config_dump @@ -384,7 +387,13 @@ end subroutine meta_config_dump ! Ensure the names of all packages are always defined subroutine meta_config_final(self) - type(metapackage_config_t), intent(inout) :: self + type(metapackage_config_t), intent(inout) :: self + call meta_config_reset(self) + end subroutine meta_config_final + + ! Ensure the names of all packages are always defined + subroutine meta_config_reset(self) + class(metapackage_config_t), intent(inout) :: self call request_destroy(self%openmp); self%openmp%name = "openmp" call request_destroy(self%stdlib); self%stdlib%name = "stdlib" @@ -394,7 +403,7 @@ subroutine meta_config_final(self) call request_destroy(self%netcdf); self%netcdf%name = "netcdf" call request_destroy(self%blas); self%blas%name = "blas" - end subroutine meta_config_final + end subroutine meta_config_reset subroutine meta_config_load(self, table, error) class(metapackage_config_t), intent(inout) :: self diff --git a/src/fpm/manifest/package.f90 b/src/fpm/manifest/package.f90 index 624e519a23..6bd8f75dc0 100644 --- a/src/fpm/manifest/package.f90 +++ b/src/fpm/manifest/package.f90 @@ -36,7 +36,7 @@ module fpm_manifest_package use fpm_manifest_build, only: build_config_t, new_build_config use fpm_manifest_dependency, only : dependency_config_t, new_dependencies - use fpm_manifest_profile, only : profile_config_t, new_profiles + use fpm_manifest_profile, only : profile_config_t, new_profiles, add_default_profiles use fpm_manifest_example, only : example_config_t, new_example use fpm_manifest_executable, only : executable_config_t, new_executable use fpm_manifest_fortran, only : fortran_config_t, new_fortran_config @@ -45,7 +45,7 @@ module fpm_manifest_package use fpm_manifest_test, only : test_config_t, new_test use fpm_manifest_preprocess, only : preprocess_config_t, new_preprocessors use fpm_manifest_feature, only: feature_config_t, init_feature_components - use fpm_manifest_feature_collection, only: feature_collection_t, get_default_features, new_collections + use fpm_manifest_feature_collection, only: feature_collection_t, new_collections, add_default_features use fpm_manifest_platform, only: platform_config_t use fpm_strings, only: string_t use fpm_filesystem, only : exists, getline, join_path @@ -92,10 +92,19 @@ module fpm_manifest_package procedure :: serializable_is_same => manifest_is_same procedure :: dump_to_toml procedure :: load_from_toml + + !> Check if any features has a cpp configuration + procedure :: has_cpp !> Export package configuration with features applied procedure :: export_config + !> Find feature by name, returns index or 0 if not found + procedure :: find_feature + + !> Find profile by name, returns index or 0 if not found + procedure :: find_profile + end type package_config_t character(len=*), parameter, private :: class_name = 'package_config_t' @@ -127,6 +136,9 @@ subroutine new_package(self, table, root, error) type(toml_array), pointer :: children character(len=:), allocatable :: version, version_file integer :: ii, nn, stat, io + + ! Ensure metapackage data is initialized although off + call self%meta%reset() call check(table, error) if (allocated(error)) return @@ -188,7 +200,7 @@ subroutine new_package(self, table, root, error) call new_profiles(self%profiles, child, error) if (allocated(error)) return else - ! Leave profiles unallocated for now + ! No profiles defined - start with empty array allocate(self%profiles(0)) end if @@ -198,11 +210,21 @@ subroutine new_package(self, table, root, error) call new_collections(self%features, child, error) if (allocated(error)) return else - ! Initialize with default feature collections (debug and release) - call get_default_features(self%features, error) - if (allocated(error)) return + ! No features defined - start with empty array + allocate(self%features(0)) end if + ! Add default features and profiles if they don't already exist + call add_default_features(self%features, error) + if (allocated(error)) return + + call add_default_profiles(self%profiles, error) + if (allocated(error)) return + + ! Validate profiles after all features and profiles have been loaded + call validate_profiles(self, error) + if (allocated(error)) return + end subroutine new_package @@ -538,20 +560,68 @@ subroutine load_from_toml(self, table, error) end subroutine load_from_toml !> Export package configuration for a given (OS+compiler) platform - type(package_config_t) function export_config(self, platform, features) result(cfg) + type(package_config_t) function export_config(self, platform, features, profile, error) result(cfg) !> Instance of the package configuration - class(package_config_t), intent(in) :: self + class(package_config_t), intent(in), target :: self !> Target platform type(platform_config_t), intent(in) :: platform - !> Optional list of features to apply (currently idle) - type(string_t), optional, intent(in) :: features(:) + !> Optional list of features to apply (cannot be used with profile) + type(string_t), optional, intent(in), target :: features(:) + + !> Optional profile name to apply (cannot be used with features) + character(len=*), optional, intent(in) :: profile + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + integer :: i, idx + type(string_t), pointer :: want_features(:) + + ! Validate that both profile and features are not specified simultaneously + if (present(profile) .and. present(features)) then + call syntax_error(error, "Cannot specify both 'profile' and 'features' parameters simultaneously") + return + end if ! Copy the entire package configuration cfg = self + ! TODO: Feature processing will be implemented here + ! For now, features parameter is ignored as requested + if (present(features)) then + want_features => features + elseif (present(profile)) then + idx = find_profile(self, profile) + if (idx<=0) then + call fatal_error(error, "Cannot find profile "//profile//" in package "//self%name) + return + end if + want_features => self%profiles(idx)%features + else + nullify(want_features) + endif + + apply_features: if (associated(want_features)) then + do i=1,size(want_features) + + ! Find feature + idx = self%find_feature(want_features(i)%s) + if (idx<=0) then + call fatal_error(error, "Cannot find feature "//want_features(i)%s//& + " in package "//self%name) + return + end if + + ! Add it to the current configuration + call self%features(idx)%merge_into_package(cfg, platform, error) + if (allocated(error)) return + + end do + end if apply_features + ! Ensure allocatable fields are always allocated with default values if not already set if (.not. allocated(cfg%build)) then allocate(cfg%build) @@ -572,12 +642,130 @@ type(package_config_t) function export_config(self, platform, features) result(c cfg%fortran%implicit_typing = .false. cfg%fortran%implicit_external = .false. cfg%fortran%source_form = 'free' - end if - - ! TODO: Feature processing will be implemented here - ! For now, features parameter is ignored as requested + end if end function export_config + !> Find profile by name, returns index or 0 if not found + function find_profile(self, profile_name) result(idx) + + !> Instance of the package configuration + class(package_config_t), intent(in) :: self + + !> Name of the feature to find + character(len=*), intent(in) :: profile_name + + !> Index of the feature (0 if not found) + integer :: idx + + integer :: i + + idx = 0 + + ! Check if features are allocated + if (.not. allocated(self%profiles)) return + + ! Search through features array + do i = 1, size(self%profiles) + if (allocated(self%profiles(i)%name)) then + if (self%profiles(i)%name == profile_name) then + idx = i + return + end if + end if + end do + + end function find_profile + + !> Find feature by name, returns index or 0 if not found + function find_feature(self, feature_name) result(idx) + + !> Instance of the package configuration + class(package_config_t), intent(in) :: self + + !> Name of the feature to find + character(len=*), intent(in) :: feature_name + + !> Index of the feature (0 if not found) + integer :: idx + + integer :: i + + idx = 0 + + ! Check if features are allocated + if (.not. allocated(self%features)) return + + ! Search through features array + do i = 1, size(self%features) + if (allocated(self%features(i)%base%name)) then + if (self%features(i)%base%name == feature_name) then + idx = i + return + end if + end if + end do + + end function find_feature + + + !> Validate profiles - check for duplicate names and valid feature references + subroutine validate_profiles(self, error) + + !> Instance of the package configuration + class(package_config_t), intent(in) :: self + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + integer :: i, j + + ! Check if profiles are allocated + if (.not. allocated(self%profiles)) return + + ! Check for duplicate profile names + do i = 1, size(self%profiles) + do j = i + 1, size(self%profiles) + if (allocated(self%profiles(i)%name) .and. allocated(self%profiles(j)%name)) then + if (self%profiles(i)%name == self%profiles(j)%name) then + call syntax_error(error, "Duplicate profile name '" // self%profiles(i)%name // "'") + return + end if + end if + end do + end do + + ! Check that all profile features reference valid features + do i = 1, size(self%profiles) + if (allocated(self%profiles(i)%features)) then + do j = 1, size(self%profiles(i)%features) + ! Check if feature exists (case sensitive) + if (self%find_feature(self%profiles(i)%features(j)%s) == 0) then + call syntax_error(error, "Profile '" // self%profiles(i)%name // & + "' references undefined feature '" // self%profiles(i)%features(j)%s // "'") + return + end if + end do + end if + end do + + end subroutine validate_profiles + + !> Check if there is a CPP preprocessor configuration + elemental logical function has_cpp(self) + class(package_config_t), intent(in) :: self + + integer :: i + + has_cpp = self%feature_config_t%has_cpp() + if (has_cpp) return + if (.not.allocated(self%features)) return + + do i=1,size(self%features) + has_cpp = self%features(i)%has_cpp() + if (has_cpp) return + end do + + end function has_cpp end module fpm_manifest_package diff --git a/src/fpm/manifest/platform.f90 b/src/fpm/manifest/platform.f90 index 96641e4e83..30f895d9ce 100644 --- a/src/fpm/manifest/platform.f90 +++ b/src/fpm/manifest/platform.f90 @@ -15,7 +15,7 @@ module fpm_manifest_platform OS_WINDOWS, OS_LINUX, OS_MACOS use fpm_compiler, only : compiler_enum, compiler_id_name, match_compiler_type, id_all, & id_unknown, validate_compiler_name, id_intel_classic_nix, id_intel_classic_mac, & - id_intel_classic_windows, id_intel_llvm_nix, id_intel_llvm_windows + id_intel_classic_windows, id_intel_llvm_nix, id_intel_llvm_windows, id_intel_llvm_unknown use fpm_strings, only : lower implicit none private @@ -26,7 +26,7 @@ module fpm_manifest_platform !> Shortcuts for the Intel OS variants integer(compiler_enum), parameter :: & id_intel_classic(*) = [id_intel_classic_mac,id_intel_classic_nix,id_intel_classic_windows], & - id_intel_llvm (*) = [id_intel_llvm_nix,id_intel_llvm_windows] + id_intel_llvm (*) = [id_intel_llvm_nix,id_intel_llvm_windows,id_intel_llvm_unknown] !> Serializable platform configuration (compiler + OS only) type, extends(serializable_t) :: platform_config_t @@ -251,10 +251,13 @@ logical function platform_is_suitable(self, target) result(ok) type(platform_config_t), intent(in) :: target logical :: compiler_ok, os_ok + + ! Check that both platforms are valid if (.not. self%is_valid() .or. .not. target%is_valid()) then ok = .false. + print *, 'compare platform ',self%name(),' with target ',target%name(),': ok=',ok return end if @@ -264,6 +267,8 @@ logical function platform_is_suitable(self, target) result(ok) ! Basic matching ok = compiler_ok .and. os_ok + if (.not.ok) print *, 'compare platform ',self%name(),' with target ',target%name(),': ok=',ok + if (.not. ok) return ! Additional validation: Intel compilers must have compatible OS @@ -273,6 +278,8 @@ logical function platform_is_suitable(self, target) result(ok) compiler_os_compatible(target%compiler, target%os_type) end if + print *, 'compare platform ',self%name(),' with target ',target%name(),': ok=',ok + end function platform_is_suitable !> Check if a platform configuration is valid (no unknowns, compatible compiler+OS) diff --git a/src/fpm/manifest/preprocess.f90 b/src/fpm/manifest/preprocess.f90 index 10cde2fcc9..8cd2c3a016 100644 --- a/src/fpm/manifest/preprocess.f90 +++ b/src/fpm/manifest/preprocess.f90 @@ -340,7 +340,7 @@ subroutine add_config(this,that) end subroutine add_config ! Check cpp - logical function is_cpp(this) + elemental logical function is_cpp(this) class(preprocess_config_t), intent(in) :: this is_cpp = .false. if (allocated(this%name)) is_cpp = this%name == "cpp" diff --git a/src/fpm/manifest/profiles.f90 b/src/fpm/manifest/profiles.f90 index b84a97d98d..36133f2ab2 100644 --- a/src/fpm/manifest/profiles.f90 +++ b/src/fpm/manifest/profiles.f90 @@ -1,662 +1,131 @@ -!> Implementation of the meta data for compiler flag profiles. +!> Implementation of the profiles configuration. !> -!> A profiles table can currently have the following subtables: -!> Profile names - any string, if omitted, flags are appended to all matching profiles -!> Compiler - any from the following list, omitting it yields an error +!> A profile is a named collection of features that can be applied together. +!> Profiles provide a convenient way to group features for different use cases, +!> such as debug builds, release builds, or specific target configurations. !> -!> - "gfortran" -!> - "ifort" -!> - "ifx" -!> - "pgfortran" -!> - "nvfortran" -!> - "flang" -!> - "caf" -!> - "f95" -!> - "lfortran" -!> - "lfc" -!> - "nagfor" -!> - "crayftn" -!> - "xlf90" -!> - "ftn95" -!> -!> OS - any from the following list, if omitted, the profile is used if and only -!> if there is no profile perfectly matching the current configuration -!> -!> - "linux" -!> - "macos" -!> - "windows" -!> - "cygwin" -!> - "solaris" -!> - "freebsd" -!> - "openbsd" -!> - "unknown" -!> -!> Each of the subtables currently supports the following fields: +!> A profile table has the following structure: !>```toml -!>[profiles.debug.gfortran.linux] -!> flags="-Wall -g -Og" -!> c-flags="-g O1" -!> cxx-flags="-g O1" -!> link-time-flags="-xlinkopt" -!> files={"hello_world.f90"="-Wall -O3"} -!>``` +!>[profiles.debug] +!>features = ["debug-flags", "development-tools"] !> +!>[profiles.release] +!>features = ["optimized", "strip-symbols"] +!>``` module fpm_manifest_profile - use fpm_manifest_feature, only: feature_config_t, new_feature, find_feature - use fpm_error, only : error_t, syntax_error, fatal_error, fpm_stop - use tomlf, only : toml_table, toml_key, toml_stat - use fpm_toml, only : get_value, serializable_t, set_value, & - set_string, add_table - use fpm_strings, only: lower - use fpm_manifest_platform, only: platform_config_t - use fpm_environment, only: get_os_type, OS_UNKNOWN, OS_LINUX, OS_MACOS, OS_WINDOWS, & - OS_CYGWIN, OS_SOLARIS, OS_FREEBSD, OS_OPENBSD, OS_NAME, OS_ALL, & - validate_os_name, match_os_type - use fpm_compiler, only: compiler_enum, compiler_id_name, match_compiler_type, & - validate_compiler_name - use fpm_filesystem, only: join_path + use fpm_error, only: error_t, fatal_error, syntax_error + use fpm_strings, only: string_t, operator(==) + use tomlf, only: toml_table, toml_array, toml_key, toml_stat, len + use fpm_toml, only: get_value, serializable_t, set_string, set_list, get_list, add_table + implicit none - public :: profile_config_t, new_profile, new_profiles, find_profile, DEFAULT_COMPILER - - !> Name of the default compiler - character(len=*), parameter :: DEFAULT_COMPILER = 'gfortran' - character(len=:), allocatable :: path - - !> Type storing file name - file scope compiler flags pairs - type, extends(serializable_t) :: file_scope_flag + private - !> Name of the file - character(len=:), allocatable :: file_name + public :: profile_config_t, new_profile, new_profiles, get_default_profiles, add_default_profiles - !> File scope flags - character(len=:), allocatable :: flags - - contains - - !> Serialization interface - procedure :: serializable_is_same => file_scope_same - procedure :: dump_to_toml => file_scope_dump - procedure :: load_from_toml => file_scope_load - - end type file_scope_flag - - !> Configuration meta data for a profile (now based on features) + !> Configuration data for a profile type, extends(serializable_t) :: profile_config_t - - !> Profile feature - contains all profile configuration - type(feature_config_t) :: profile_feature - !> File scope flags (maintained for backwards compatibility) - type(file_scope_flag), allocatable :: file_scope_flags(:) + !> Profile name + character(len=:), allocatable :: name + + !> List of features to apply + type(string_t), allocatable :: features(:) - contains + contains !> Print information on this instance procedure :: info !> Serialization interface - procedure :: serializable_is_same => profile_same - procedure :: dump_to_toml => profile_dump - procedure :: load_from_toml => profile_load - - !> Convenience accessors for backward compatibility - procedure :: profile_name => get_profile_name - procedure :: compiler => get_profile_compiler - procedure :: os_type => get_profile_os_type - procedure :: flags => get_profile_flags - procedure :: c_flags => get_profile_c_flags - procedure :: cxx_flags => get_profile_cxx_flags - procedure :: link_time_flags => get_profile_link_time_flags - procedure :: is_built_in => get_profile_is_built_in + procedure :: serializable_is_same => profile_is_same + procedure :: dump_to_toml + procedure :: load_from_toml end type profile_config_t - contains - - !> Construct a new profile configuration from a TOML data structure - function new_profile(profile_name, compiler, os_type, flags, c_flags, cxx_flags, & - link_time_flags, file_scope_flags, is_built_in) & - & result(profile) - - !> Name of the profile - character(len=*), intent(in) :: profile_name - - !> Name of the compiler - character(len=*), intent(in) :: compiler - - !> Type of the OS - integer, intent(in) :: os_type - - !> Fortran compiler flags - character(len=*), optional, intent(in) :: flags - - !> C compiler flags - character(len=*), optional, intent(in) :: c_flags - - !> C++ compiler flags - character(len=*), optional, intent(in) :: cxx_flags - - !> Link time compiler flags - character(len=*), optional, intent(in) :: link_time_flags - - !> File scope flags - type(file_scope_flag), optional, intent(in) :: file_scope_flags(:) + character(len=*), parameter, private :: class_name = 'profile_config_t' - !> Is this profile one of the built-in ones? - logical, optional, intent(in) :: is_built_in +contains - type(profile_config_t) :: profile - integer(compiler_enum) :: compiler_id + !> Construct a new profile configuration from a TOML array + subroutine new_profile(self, features_array, profile_name, error) - ! Initialize the profile feature - profile%profile_feature%name = profile_name - profile%profile_feature%platform = platform_config_t(compiler, os_type) - if (present(is_built_in)) then - profile%profile_feature%default = is_built_in - else - profile%profile_feature%default = .false. - end if - - ! Set flags - if (present(flags)) then - profile%profile_feature%flags = flags - else - profile%profile_feature%flags = "" - end if - if (present(c_flags)) then - profile%profile_feature%c_flags = c_flags - else - profile%profile_feature%c_flags = "" - end if - if (present(cxx_flags)) then - profile%profile_feature%cxx_flags = cxx_flags - else - profile%profile_feature%cxx_flags = "" - end if - if (present(link_time_flags)) then - profile%profile_feature%link_time_flags = link_time_flags - else - profile%profile_feature%link_time_flags = "" - end if - - ! Set file scope flags (maintained for backward compatibility) - if (present(file_scope_flags)) then - profile%file_scope_flags = file_scope_flags - end if - - end function new_profile - - !> Match lowercase string with name of OS to os_type enum - function os_type_name(os_type) - - !> Name of operating system - character(len=:), allocatable :: os_type_name - - !> Enum representing type of OS - integer, intent(in) :: os_type - - select case (os_type) - case (OS_ALL); os_type_name = "all" - case default; os_type_name = lower(OS_NAME(os_type)) - end select - - end function os_type_name - - subroutine validate_profile_table(profile_name, compiler_name, key_list, table, error, os_valid) - - !> Name of profile - character(len=:), allocatable, intent(in) :: profile_name - - !> Name of compiler - character(len=:), allocatable, intent(in) :: compiler_name + !> Instance of the profile configuration + type(profile_config_t), intent(out) :: self - !> List of keys in the table - type(toml_key), allocatable, intent(in) :: key_list(:) + !> TOML array containing the feature names + type(toml_array), intent(inout) :: features_array - !> Table containing OS tables - type(toml_table), pointer, intent(in) :: table + !> Name of the profile + character(len=*), intent(in) :: profile_name !> Error handling type(error_t), allocatable, intent(out) :: error - !> Was called with valid operating system - logical, intent(in) :: os_valid - - character(len=:), allocatable :: flags, c_flags, cxx_flags, link_time_flags, key_name, file_name, file_flags, err_message - type(toml_table), pointer :: files - type(toml_key), allocatable :: file_list(:) - integer :: ikey, ifile, stat - logical :: is_valid - - if (size(key_list).ge.1) then - do ikey=1,size(key_list) - key_name = key_list(ikey)%key - if (key_name.eq.'flags') then - call get_value(table, 'flags', flags, stat=stat) - if (stat /= toml_stat%success) then - call syntax_error(error, "flags has to be a key-value pair") - return - end if - else if (key_name.eq.'c-flags') then - call get_value(table, 'c-flags', c_flags, stat=stat) - if (stat /= toml_stat%success) then - call syntax_error(error, "c-flags has to be a key-value pair") - return - end if - else if (key_name.eq.'cxx-flags') then - call get_value(table, 'cxx-flags', cxx_flags, stat=stat) - if (stat /= toml_stat%success) then - call syntax_error(error, "cxx-flags has to be a key-value pair") - return - end if - else if (key_name.eq.'link-time-flags') then - call get_value(table, 'link-time-flags', link_time_flags, stat=stat) - if (stat /= toml_stat%success) then - call syntax_error(error, "link-time-flags has to be a key-value pair") - return - end if - else if (key_name.eq.'files') then - call get_value(table, 'files', files, stat=stat) - if (stat /= toml_stat%success) then - call syntax_error(error, "files has to be a table") - return - end if - call files%get_keys(file_list) - do ifile=1,size(file_list) - file_name = file_list(ifile)%key - call get_value(files, file_name, file_flags, stat=stat) - if (stat /= toml_stat%success) then - call syntax_error(error, "file scope flags has to be a key-value pair") - return - end if - end do - else if (.not. os_valid) then - call validate_os_name(key_name, is_valid) - err_message = "Unexpected key " // key_name // " found in profile table "//profile_name//" "//compiler_name//"." - if (.not. is_valid) call syntax_error(error, err_message) - else - err_message = "Unexpected key " // key_name // " found in profile table "//profile_name//" "//compiler_name//"." - call syntax_error(error, err_message) - end if - end do - end if - - if (allocated(error)) return - - end subroutine validate_profile_table - - !> Look for flags, c-flags, link-time-flags key-val pairs - !> and files table in a given table and create new profiles - subroutine get_flags(profile_name, compiler_name, os_type, key_list, table, profiles, profindex, os_valid) - - !> Name of profile - character(len=:), allocatable, intent(in) :: profile_name + integer :: i, stat + character(len=:), allocatable :: feature_name - !> Name of compiler - character(len=:), allocatable, intent(in) :: compiler_name + ! Set profile name + self%name = profile_name - !> OS type - integer, intent(in) :: os_type - - !> List of keys in the table - type(toml_key), allocatable, intent(in) :: key_list(:) - - !> Table containing OS tables - type(toml_table), pointer, intent(in) :: table - - !> List of profiles - type(profile_config_t), allocatable, intent(inout) :: profiles(:) - - !> Index in the list of profiles - integer, intent(inout) :: profindex - - !> Was called with valid operating system - logical, intent(in) :: os_valid - - character(len=:), allocatable :: flags, c_flags, cxx_flags, link_time_flags, key_name, file_name, file_flags, err_message - type(toml_table), pointer :: files - type(toml_key), allocatable :: file_list(:) - type(file_scope_flag), allocatable :: file_scope_flags(:) - integer :: ikey, ifile, stat - logical :: is_valid - - call get_value(table, 'flags', flags) - call get_value(table, 'c-flags', c_flags) - call get_value(table, 'cxx-flags', cxx_flags) - call get_value(table, 'link-time-flags', link_time_flags) - call get_value(table, 'files', files) - if (associated(files)) then - call files%get_keys(file_list) - allocate(file_scope_flags(size(file_list))) - do ifile=1,size(file_list) - file_name = file_list(ifile)%key - call get_value(files, file_name, file_flags) - associate(cur_file=>file_scope_flags(ifile)) - if (.not.(path.eq."")) file_name = join_path(path, file_name) - cur_file%file_name = file_name - cur_file%flags = file_flags - end associate - end do + ! Get feature names from array + if (len(features_array) > 0) then + allocate(self%features(len(features_array))) + do i = 1, len(features_array) + call get_value(features_array, i, feature_name, stat=stat) + if (stat /= toml_stat%success) then + call fatal_error(error, "Failed to read feature name from profile " // profile_name) + return + end if + self%features(i)%s = feature_name + end do + else + allocate(self%features(0)) end if - profiles(profindex) = new_profile(profile_name, compiler_name, os_type, & - & flags, c_flags, cxx_flags, link_time_flags, file_scope_flags) - profindex = profindex + 1 - end subroutine get_flags - - !> Traverse operating system tables to obtain number of profiles - subroutine traverse_oss_for_size(profile_name, compiler_name, os_list, table, profiles_size, error) - - !> Name of profile - character(len=:), allocatable, intent(in) :: profile_name - - !> Name of compiler - character(len=:), allocatable, intent(in) :: compiler_name + end subroutine new_profile - !> List of OSs in table with profile name and compiler name given - type(toml_key), allocatable, intent(in) :: os_list(:) - !> Table containing OS tables - type(toml_table), pointer, intent(in) :: table + !> Construct new profiles array from a TOML data structure + subroutine new_profiles(profiles, table, error) - !> Error handling - type(error_t), allocatable, intent(out) :: error - - !> Number of profiles in list of profiles - integer, intent(inout) :: profiles_size - - type(toml_key), allocatable :: key_list(:) - character(len=:), allocatable :: os_name, l_os_name - type(toml_table), pointer :: os_node - integer :: ios, stat - logical :: is_valid, key_val_added, is_key_val - - if (size(os_list)<1) return - key_val_added = .false. - do ios = 1, size(os_list) - os_name = os_list(ios)%key - call validate_os_name(os_name, is_valid) - if (is_valid) then - call get_value(table, os_name, os_node, stat=stat) - if (stat /= toml_stat%success) then - call syntax_error(error, "os "//os_name//" has to be a table") - return - end if - call os_node%get_keys(key_list) - profiles_size = profiles_size + 1 - call validate_profile_table(profile_name, compiler_name, key_list, os_node, error, .true.) - else - ! Not lowercase OS name - l_os_name = lower(os_name) - call validate_os_name(l_os_name, is_valid) - if (is_valid) then - call fatal_error(error,'*traverse_oss*:Error: Name of the operating system must be a lowercase string.') - end if - if (allocated(error)) return - - ! Missing OS name - is_key_val = .false. - os_name = os_list(ios)%key - call get_value(table, os_name, os_node, stat=stat) - if (stat /= toml_stat%success) then - is_key_val = .true. - end if - os_node=>table - if (is_key_val.and..not.key_val_added) then - key_val_added = .true. - is_key_val = .false. - profiles_size = profiles_size + 1 - else if (.not.is_key_val) then - profiles_size = profiles_size + 1 - end if - call validate_profile_table(profile_name, compiler_name, os_list, os_node, error, .false.) - end if - end do - end subroutine traverse_oss_for_size - - - !> Traverse operating system tables to obtain profiles - subroutine traverse_oss(profile_name, compiler_name, os_list, table, profiles, profindex, error) - - !> Name of profile - character(len=:), allocatable, intent(in) :: profile_name - - !> Name of compiler - character(len=:), allocatable, intent(in) :: compiler_name - - !> List of OSs in table with profile name and compiler name given - type(toml_key), allocatable, intent(in) :: os_list(:) - - !> Table containing OS tables - type(toml_table), pointer, intent(in) :: table - - !> Error handling - type(error_t), allocatable, intent(out) :: error - - !> List of profiles - type(profile_config_t), allocatable, intent(inout) :: profiles(:) - - !> Index in the list of profiles - integer, intent(inout) :: profindex - - type(toml_key), allocatable :: key_list(:) - character(len=:), allocatable :: os_name, l_os_name - type(toml_table), pointer :: os_node - integer :: ios, stat, os_type - logical :: is_valid, is_key_val - - if (size(os_list)<1) return - do ios = 1, size(os_list) - os_name = os_list(ios)%key - call validate_os_name(os_name, is_valid) - if (is_valid) then - call get_value(table, os_name, os_node, stat=stat) - if (stat /= toml_stat%success) then - call syntax_error(error, "os "//os_name//" has to be a table") - return - end if - call os_node%get_keys(key_list) - os_type = match_os_type(os_name) - call get_flags(profile_name, compiler_name, os_type, key_list, os_node, profiles, profindex, .true.) - else - ! Not lowercase OS name - l_os_name = lower(os_name) - call validate_os_name(l_os_name, is_valid) - if (is_valid) then - call fatal_error(error,'*traverse_oss*:Error: Name of the operating system must be a lowercase string.') - end if - if (allocated(error)) return - - ! Missing OS name - is_key_val = .false. - os_name = os_list(ios)%key - call get_value(table, os_name, os_node, stat=stat) - if (stat /= toml_stat%success) then - is_key_val = .true. - end if - os_node=>table - os_type = OS_ALL - call get_flags(profile_name, compiler_name, os_type, os_list, os_node, profiles, profindex, .false.) - end if - end do - end subroutine traverse_oss - - !> Traverse compiler tables - subroutine traverse_compilers(profile_name, comp_list, table, error, profiles_size, profiles, profindex) - - !> Name of profile - character(len=:), allocatable, intent(in) :: profile_name - - !> List of OSs in table with profile name given - type(toml_key), allocatable, intent(in) :: comp_list(:) - - !> Table containing compiler tables - type(toml_table), pointer, intent(in) :: table - - !> Error handling - type(error_t), allocatable, intent(out) :: error - - !> Number of profiles in list of profiles - integer, intent(inout), optional :: profiles_size - - !> List of profiles - type(profile_config_t), allocatable, intent(inout), optional :: profiles(:) - - !> Index in the list of profiles - integer, intent(inout), optional :: profindex - - character(len=:), allocatable :: compiler_name - type(toml_table), pointer :: comp_node - type(toml_key), allocatable :: os_list(:) - integer :: icomp, stat - logical :: is_valid - - if (size(comp_list)<1) return - do icomp = 1, size(comp_list) - call validate_compiler_name(comp_list(icomp)%key, is_valid) - if (is_valid) then - compiler_name = comp_list(icomp)%key - call get_value(table, compiler_name, comp_node, stat=stat) - if (stat /= toml_stat%success) then - call syntax_error(error, "Compiler "//comp_list(icomp)%key//" must be a table entry") - exit - end if - call comp_node%get_keys(os_list) - if (present(profiles_size)) then - call traverse_oss_for_size(profile_name, compiler_name, os_list, comp_node, profiles_size, error) - if (allocated(error)) return - else - if (.not.(present(profiles).and.present(profindex))) then - call fatal_error(error, "Both profiles and profindex have to be present") - return - end if - call traverse_oss(profile_name, compiler_name, os_list, comp_node, & - & profiles, profindex, error) - if (allocated(error)) return - end if - else - call fatal_error(error,'*traverse_compilers*:Error: Compiler name not specified or invalid.') - end if - end do - end subroutine traverse_compilers - - !> Construct new profiles array from a TOML data structure - subroutine new_profiles(profiles, table, error) - - !> Instance of the dependency configuration + !> Instance of the profile configuration array type(profile_config_t), allocatable, intent(out) :: profiles(:) !> Instance of the TOML data structure - type(toml_table), target, intent(inout) :: table + type(toml_table), intent(inout) :: table !> Error handling type(error_t), allocatable, intent(out) :: error - type(toml_table), pointer :: prof_node - type(toml_key), allocatable :: prof_list(:) - type(toml_key), allocatable :: comp_list(:) - type(toml_key), allocatable :: os_list(:) - character(len=:), allocatable :: profile_name, compiler_name - integer :: profiles_size, iprof, stat, profindex - logical :: is_valid - type(profile_config_t), allocatable :: default_profiles(:) + type(toml_array), pointer :: array_node + type(toml_key), allocatable :: list(:) + integer :: iprofile, stat - path = '' - - ! Default profiles are now features - no longer used - allocate(default_profiles(0)) - call table%get_keys(prof_list) - - if (size(prof_list) < 1) return - - profiles_size = 0 - - do iprof = 1, size(prof_list) - profile_name = prof_list(iprof)%key - call validate_compiler_name(profile_name, is_valid) - if (is_valid) then - profile_name = "all" - comp_list = prof_list(iprof:iprof) - prof_node=>table - call traverse_compilers(profile_name, comp_list, prof_node, error, profiles_size=profiles_size) - if (allocated(error)) return - else - call validate_os_name(profile_name, is_valid) - if (is_valid) then - os_list = prof_list(iprof:iprof) - profile_name = 'all' - compiler_name = DEFAULT_COMPILER - call traverse_oss_for_size(profile_name, compiler_name, os_list, table, profiles_size, error) - if (allocated(error)) return - else - call get_value(table, profile_name, prof_node, stat=stat) - if (stat /= toml_stat%success) then - call syntax_error(error, "Profile "//prof_list(iprof)%key//" must be a table entry") - exit - end if - call prof_node%get_keys(comp_list) - call traverse_compilers(profile_name, comp_list, prof_node, error, profiles_size=profiles_size) - if (allocated(error)) return - end if - end if - end do + call table%get_keys(list) - profiles_size = profiles_size + size(default_profiles) - allocate(profiles(profiles_size)) + if (size(list) < 1) then + allocate(profiles(0)) + return + end if - do profindex=1, size(default_profiles) - profiles(profindex) = default_profiles(profindex) - end do + allocate(profiles(size(list))) - do iprof = 1, size(prof_list) - profile_name = prof_list(iprof)%key - call validate_compiler_name(profile_name, is_valid) - if (is_valid) then - profile_name = "all" - comp_list = prof_list(iprof:iprof) - prof_node=>table - call traverse_compilers(profile_name, comp_list, prof_node, error, profiles=profiles, profindex=profindex) - if (allocated(error)) return - else - call validate_os_name(profile_name, is_valid) - if (is_valid) then - os_list = prof_list(iprof:iprof) - profile_name = 'all' - compiler_name = DEFAULT_COMPILER - prof_node=>table - call traverse_oss(profile_name, compiler_name, os_list, prof_node, profiles, profindex, error) - if (allocated(error)) return - else - call get_value(table, profile_name, prof_node, stat=stat) - call prof_node%get_keys(comp_list) - call traverse_compilers(profile_name, comp_list, prof_node, error, profiles=profiles, profindex=profindex) - if (allocated(error)) return + do iprofile = 1, size(list) + call get_value(table, list(iprofile)%key, array_node, stat=stat) + if (stat /= toml_stat%success) then + call fatal_error(error, "Profile "//list(iprofile)%key//" must be an array of feature names") + exit end if - end if + call new_profile(profiles(iprofile), array_node, list(iprofile)%key, error) + if (allocated(error)) exit end do - ! Apply profiles with profile name 'all' to matching profiles - do iprof = 1,size(profiles) - if (profiles(iprof)%profile_feature%name == 'all') then - do profindex = 1,size(profiles) - if (.not.(profiles(profindex)%profile_feature%name == 'all') & - & .and.(profiles(profindex)%profile_feature%platform == profiles(iprof)%profile_feature%platform)) then - profiles(profindex)%profile_feature%flags = profiles(profindex)%profile_feature%flags // & - & " " // profiles(iprof)%profile_feature%flags - profiles(profindex)%profile_feature%c_flags = profiles(profindex)%profile_feature%c_flags // & - & " " // profiles(iprof)%profile_feature%c_flags - profiles(profindex)%profile_feature%cxx_flags = profiles(profindex)%profile_feature%cxx_flags // & - & " " // profiles(iprof)%profile_feature%cxx_flags - profiles(profindex)%profile_feature%link_time_flags = profiles(profindex)%profile_feature%link_time_flags // & - & " " // profiles(iprof)%profile_feature%link_time_flags - end if - end do - end if - end do - end subroutine new_profiles + end subroutine new_profiles - !> Write information on instance - subroutine info(self, unit, verbosity) + !> Write information on instance + subroutine info(self, unit, verbosity) !> Instance of the profile configuration class(profile_config_t), intent(in) :: self @@ -667,8 +136,9 @@ subroutine info(self, unit, verbosity) !> Verbosity of the printout integer, intent(in), optional :: verbosity - integer :: pr - character(len=*), parameter :: fmt = '("#", 1x, a, t30, a)' + integer :: pr, ii + character(len=*), parameter :: fmt = '("#", 1x, a, t30, a)', & + & fmti = '("#", 1x, a, t30, i0)' if (present(verbosity)) then pr = verbosity @@ -676,114 +146,64 @@ subroutine info(self, unit, verbosity) pr = 1 end if + if (pr < 1) return + write(unit, fmt) "Profile" - if (allocated(self%profile_feature%name)) then - write(unit, fmt) "- profile name", self%profile_feature%name + if (allocated(self%name)) then + write(unit, fmt) "- name", self%name end if - - call self%profile_feature%platform%info(unit, verbosity) - if (allocated(self%profile_feature%flags)) then - write(unit, fmt) "- compiler flags", self%profile_feature%flags + if (allocated(self%features)) then + if (size(self%features) > 0) then + write(unit, fmti) "- features", size(self%features) + if (pr > 1) then + do ii = 1, size(self%features) + write(unit, fmt) " - feature", self%features(ii)%s + end do + end if + end if end if - end subroutine info + end subroutine info - !> Look for profile with given configuration in array profiles - subroutine find_profile(profiles, profile_name, target, found_matching, chosen_profile) + !> Check that two profile configs are equal + logical function profile_is_same(this, that) + class(profile_config_t), intent(in) :: this + class(serializable_t), intent(in) :: that - !> Array of profiles - type(profile_config_t), allocatable, intent(in) :: profiles(:) + integer :: ii - !> Name of profile - character(:), allocatable, intent(in) :: profile_name + profile_is_same = .false. - ! Target platform - type(platform_config_t), intent(in) :: target + select type (other=>that) + type is (profile_config_t) - !> Boolean value containing true if matching profile was found - logical, intent(out) :: found_matching - - !> Last matching profile in the profiles array - type(profile_config_t), intent(out) :: chosen_profile - - integer :: i - - found_matching = .false. - if (size(profiles) < 1) return - - - ! Try to find profile with matching OS type - do i=1,size(profiles) - - associate (feat => profiles(i)%profile_feature) - - if (profiles(i)%profile_feature%name == profile_name) then - if (profiles(i)%profile_feature%platform%matches(target)) then - chosen_profile = profiles(i) - found_matching = .true. - return - end if - end if - - endassociate - - end do - - end subroutine find_profile - - - logical function file_scope_same(this,that) - class(file_scope_flag), intent(in) :: this - class(serializable_t), intent(in) :: that - - file_scope_same = .false. + if (allocated(this%name).neqv.allocated(other%name)) return + if (allocated(this%name)) then + if (.not.(this%name==other%name)) return + end if - select type (other=>that) - type is (file_scope_flag) - if (allocated(this%file_name).neqv.allocated(other%file_name)) return - if (allocated(this%file_name)) then - if (.not.(this%file_name==other%file_name)) return - endif - if (allocated(this%flags).neqv.allocated(other%flags)) return - if (allocated(this%flags)) then - if (.not.(this%flags==other%flags)) return - endif + if (allocated(this%features).neqv.allocated(other%features)) return + if (allocated(this%features)) then + if (.not.(size(this%features)==size(other%features))) return + do ii = 1, size(this%features) + if (.not.(this%features(ii)==other%features(ii))) return + end do + end if - class default - ! Not the same type + class default return - end select - - !> All checks passed! - file_scope_same = .true. - - end function file_scope_same - - !> Dump to toml table - subroutine file_scope_dump(self, table, error) - - !> Instance of the serializable object - class(file_scope_flag), intent(inout) :: self - - !> Data structure - type(toml_table), intent(inout) :: table - - !> Error handling - type(error_t), allocatable, intent(out) :: error + end select - call set_string(table, "file-name", self%file_name, error) - if (allocated(error)) return - call set_string(table, "flags", self%flags, error) - if (allocated(error)) return + profile_is_same = .true. - end subroutine file_scope_dump + end function profile_is_same - !> Read from toml table (no checks made at this stage) - subroutine file_scope_load(self, table, error) + !> Dump profile to toml table + subroutine dump_to_toml(self, table, error) !> Instance of the serializable object - class(file_scope_flag), intent(inout) :: self + class(profile_config_t), intent(inout) :: self !> Data structure type(toml_table), intent(inout) :: table @@ -791,101 +211,16 @@ subroutine file_scope_load(self, table, error) !> Error handling type(error_t), allocatable, intent(out) :: error - call get_value(table, "file-name", self%file_name) - call get_value(table, "flags", self%flags) - - end subroutine file_scope_load - - logical function profile_same(this,that) - class(profile_config_t), intent(in) :: this - class(serializable_t), intent(in) :: that - - integer :: ii - - profile_same = .false. - - select type (other=>that) - type is (profile_config_t) - - ! Compare the underlying features - if (.not.(this%profile_feature==other%profile_feature)) return - - ! Compare file scope flags (maintained for backward compatibility) - if (allocated(this%file_scope_flags).neqv.allocated(other%file_scope_flags)) return - if (allocated(this%file_scope_flags)) then - if (.not.size(this%file_scope_flags)==size(other%file_scope_flags)) return - do ii=1,size(this%file_scope_flags) - if (.not.this%file_scope_flags(ii)==other%file_scope_flags(ii)) return - end do - endif - - class default - ! Not the same type - return - end select - - !> All checks passed! - profile_same = .true. - - end function profile_same - - !> Dump to toml table - subroutine profile_dump(self, table, error) - - !> Instance of the serializable object - class(profile_config_t), intent(inout) :: self - - !> Data structure - type(toml_table), intent(inout) :: table - - !> Error handling - type(error_t), allocatable, intent(out) :: error - - !> Local variables - integer :: ierr, ii - type(toml_table), pointer :: ptr_deps, ptr - character(len=30) :: unnamed - - ! Dump the underlying feature data - call self%profile_feature%dump_to_toml(table, error) - if (allocated(error)) return - - if (allocated(self%file_scope_flags)) then - - ! Create file scope flags table - call add_table(table, "file-scope-flags", ptr_deps) - if (.not. associated(ptr_deps)) then - call fatal_error(error, "profile_config_t cannot create file scope table ") - return - end if - - do ii = 1, size(self%file_scope_flags) - associate (dep => self%file_scope_flags(ii)) - - !> Because files need a name, fallback if this has no name - if (len_trim(dep%file_name)==0) then - write(unnamed,1) ii - call add_table(ptr_deps, trim(unnamed), ptr) - else - call add_table(ptr_deps, dep%file_name, ptr) - end if - if (.not. associated(ptr)) then - call fatal_error(error, "profile_config_t cannot create entry for file "//dep%file_name) - return - end if - call dep%dump_to_toml(ptr, error) - if (allocated(error)) return - end associate - end do - - endif + call set_string(table, "name", self%name, error, class_name) + if (allocated(error)) return - 1 format('UNNAMED_FILE_',i0) + call set_list(table, "features", self%features, error) + if (allocated(error)) return - end subroutine profile_dump + end subroutine dump_to_toml - !> Read from toml table (no checks made at this stage) - subroutine profile_load(self, table, error) + !> Read profile from toml table (no checks made at this stage) + subroutine load_from_toml(self, table, error) !> Instance of the serializable object class(profile_config_t), intent(inout) :: self @@ -896,105 +231,103 @@ subroutine profile_load(self, table, error) !> Error handling type(error_t), allocatable, intent(out) :: error - !> Local variables - character(len=:), allocatable :: flag, compiler_name - integer :: ii, jj - type(toml_table), pointer :: ptr_dep, ptr - type(toml_key), allocatable :: keys(:),dep_keys(:) + call get_value(table, "name", self%name) + + call get_list(table, "features", self%features, error) + if (allocated(error)) return + + end subroutine load_from_toml + - call table%get_keys(keys) + !> Create default profiles with standard features + subroutine get_default_profiles(profiles, error) - ! Load into feature structure - ! Dump the underlying feature data - call self%profile_feature%load_from_toml(table, error) - if (allocated(error)) return + !> Instance of the profile configuration array + type(profile_config_t), allocatable, intent(out) :: profiles(:) - if (allocated(self%file_scope_flags)) deallocate(self%file_scope_flags) - sub_deps: do ii = 1, size(keys) + !> Error handling + type(error_t), allocatable, intent(out) :: error - select case (keys(ii)%key) - case ("file-scope-flags") + ! Create two default profiles: debug and release + allocate(profiles(2)) - call get_value(table, keys(ii), ptr) - if (.not.associated(ptr)) then - call fatal_error(error,'profile_config_t: error retrieving file_scope_flags table') - return - end if + ! Debug profile with "debug" feature + profiles(1)%name = "debug" + allocate(profiles(1)%features(1)) + profiles(1)%features(1)%s = "debug" - !> Read all file scope flags - call ptr%get_keys(dep_keys) - allocate(self%file_scope_flags(size(dep_keys))) + ! Release profile with "release" feature + profiles(2)%name = "release" + allocate(profiles(2)%features(1)) + profiles(2)%features(1)%s = "release" - do jj = 1, size(dep_keys) + end subroutine get_default_profiles - call get_value(ptr, dep_keys(jj), ptr_dep) - call self%file_scope_flags(jj)%load_from_toml(ptr_dep, error) - if (allocated(error)) return - end do + !> Add default profiles to existing profiles array if they don't already exist + subroutine add_default_profiles(profiles, error) - end select - end do sub_deps + !> Instance of the profile configuration array (will be resized) + type(profile_config_t), allocatable, intent(inout) :: profiles(:) - end subroutine profile_load + !> Error handling + type(error_t), allocatable, intent(out) :: error - !> Convenience accessor procedures for backward compatibility + type(profile_config_t), allocatable :: temp_profiles(:) + type(profile_config_t), allocatable :: default_profiles(:) + logical :: debug_exists, release_exists + integer :: i, current_size, new_size - !> Get profile name - function get_profile_name(self) result(name) - class(profile_config_t), intent(in) :: self - character(len=:), allocatable :: name - name = self%profile_feature%name - end function get_profile_name + ! Get default profiles + call get_default_profiles(default_profiles, error) + if (allocated(error)) return - !> Get compiler name - function get_profile_compiler(self) result(compiler) - class(profile_config_t), intent(in) :: self - character(len=:), allocatable :: compiler - compiler = compiler_id_name(self%profile_feature%platform%compiler) - end function get_profile_compiler + ! Check if debug and release profiles already exist + debug_exists = .false. + release_exists = .false. - !> Get OS type - function get_profile_os_type(self) result(os_type) - class(profile_config_t), intent(in) :: self - integer :: os_type - os_type = self%profile_feature%platform%os_type - end function get_profile_os_type + if (allocated(profiles)) then + do i = 1, size(profiles) + if (allocated(profiles(i)%name)) then + if (profiles(i)%name == "debug") debug_exists = .true. + if (profiles(i)%name == "release") release_exists = .true. + end if + end do + current_size = size(profiles) + else + current_size = 0 + end if - !> Get flags - function get_profile_flags(self) result(flags) - class(profile_config_t), intent(in) :: self - character(len=:), allocatable :: flags - flags = self%profile_feature%flags - end function get_profile_flags + ! Calculate how many profiles to add + new_size = current_size + if (.not. debug_exists) new_size = new_size + 1 + if (.not. release_exists) new_size = new_size + 1 - !> Get C flags - function get_profile_c_flags(self) result(c_flags) - class(profile_config_t), intent(in) :: self - character(len=:), allocatable :: c_flags - c_flags = self%profile_feature%c_flags - end function get_profile_c_flags + ! If nothing to add, return + if (new_size == current_size) return - !> Get C++ flags - function get_profile_cxx_flags(self) result(cxx_flags) - class(profile_config_t), intent(in) :: self - character(len=:), allocatable :: cxx_flags - cxx_flags = self%profile_feature%cxx_flags - end function get_profile_cxx_flags + ! Create new array with existing + missing defaults + allocate(temp_profiles(new_size)) - !> Get link time flags - function get_profile_link_time_flags(self) result(link_time_flags) - class(profile_config_t), intent(in) :: self - character(len=:), allocatable :: link_time_flags - link_time_flags = self%profile_feature%link_time_flags - end function get_profile_link_time_flags + ! Copy existing profiles + if (current_size > 0) then + temp_profiles(1:current_size) = profiles(1:current_size) + end if - !> Get is_built_in flag (maps to feature default flag) - function get_profile_is_built_in(self) result(is_built_in) - class(profile_config_t), intent(in) :: self - logical :: is_built_in - is_built_in = self%profile_feature%default - end function get_profile_is_built_in + ! Add missing defaults + i = current_size + if (.not. debug_exists) then + i = i + 1 + temp_profiles(i) = default_profiles(1) ! debug profile + end if + if (.not. release_exists) then + i = i + 1 + temp_profiles(i) = default_profiles(2) ! release profile + end if + + ! Replace the profiles array + call move_alloc(temp_profiles, profiles) + end subroutine add_default_profiles end module fpm_manifest_profile diff --git a/src/fpm/toml.f90 b/src/fpm/toml.f90 index af3f93353e..ce4d91a340 100644 --- a/src/fpm/toml.f90 +++ b/src/fpm/toml.f90 @@ -821,7 +821,10 @@ subroutine check_keys(table, valid_keys, error) end if ! Check if value can be mapped or else (wrong type) show error message with the error location. - ! Right now, it can only be mapped to a string or to a child node, but this can be extended in the future. + ! Right now, it can only be mapped to a string or to a list or a child node, but this can be + ! extended in the future. + if (has_list(table, keys(ikey)%key)) cycle + call get_value(table, keys(ikey)%key, value) if (.not. allocated(value)) then diff --git a/src/fpm_command_line.f90 b/src/fpm_command_line.f90 index f871ccd232..44e13b5115 100644 --- a/src/fpm_command_line.f90 +++ b/src/fpm_command_line.f90 @@ -29,7 +29,7 @@ module fpm_command_line use M_CLI2, only : set_args, lget, sget, unnamed, remaining, specified use M_CLI2, only : get_subcommand, CLI_RESPONSE_FILE use fpm_strings, only : lower, split, to_fortran_name, is_fortran_name, remove_characters_in_set, & - string_t, glob + string_t, glob, is_valid_feature_name use fpm_filesystem, only : basename, canon_path, which, run use fpm_environment, only : get_command_arguments_quoted use fpm_error, only : fpm_stop, error_t @@ -84,6 +84,7 @@ module fpm_command_line character(len=:),allocatable :: cxx_compiler character(len=:),allocatable :: archiver character(len=:),allocatable :: profile + type(string_t), allocatable :: features(:) character(len=:),allocatable :: flag character(len=:),allocatable :: cflag character(len=:),allocatable :: cxxflag @@ -164,11 +165,14 @@ module fpm_command_line ! '12345678901234567890123456789012345678901234567890123456789012345678901234567890',& character(len=80), parameter :: help_text_build_common(*) = [character(len=80) :: & - ' --profile PROF Selects the compilation profile for the build. ',& - ' Currently available profiles are "release" for ',& - ' high optimization and "debug" for full debug options. ',& - ' If --flag is not specified the "debug" flags are the ',& + ' --profile PROF Selects either a compilation profile ("release", "debug") or ',& + ' a feature profile defined in fpm.toml. Feature profiles ',& + ' group multiple features together. Cannot be used with ',& + ' --features. If --flag is not specified the "debug" flags ',& ' default. ',& + ' --features LIST Comma-separated list of features to enable (defined in ',& + ' fpm.toml). Cannot be used with --profile. ',& + ' Example: `fpm build --features mpi,openmp,hdf5 ` ',& ' --no-prune Disable tree-shaking/pruning of unused module dependencies ',& ' --build-dir DIR Specify the build directory. Default is "build" unless set ',& ' by the environment variable FPM_BUILD_DIR. '& @@ -249,9 +253,10 @@ subroutine get_command_line_settings(cmd_settings) & c_compiler, cxx_compiler, archiver, version_s, token_s, config_file character(len=*), parameter :: fc_env = "FC", cc_env = "CC", ar_env = "AR", & - & fflags_env = "FFLAGS", cflags_env = "CFLAGS", cxxflags_env = "CXXFLAGS", ldflags_env = "LDFLAGS", & - & fc_default = "gfortran", cc_default = " ", ar_default = " ", flags_default = " ", & - & cxx_env = "CXX", cxx_default = " ", build_dir_env = "BUILD_DIR", build_dir_default = "build" + & fflags_env = "FFLAGS", cflags_env = "CFLAGS", cxxflags_env = "CXXFLAGS", & + & ldflags_env = "LDFLAGS", fc_default = "gfortran", cc_default = " ", ar_default = " ", & + & flags_default = " ", cxx_env = "CXX", cxx_default = " ", build_dir_env = "BUILD_DIR", & + & build_dir_default = "build" type(error_t), allocatable :: error call set_help() @@ -297,6 +302,7 @@ subroutine get_command_line_settings(cmd_settings) compiler_args = & ' --profile " "' // & + ' --features " "' // & ' --no-prune F' // & ' --compiler "'//get_fpm_env(fc_env, fc_default)//'"' // & ' --c-compiler "'//get_fpm_env(cc_env, cc_default)//'"' // & @@ -406,14 +412,16 @@ subroutine get_command_line_settings(cmd_settings) name='.' else write(stderr,'(*(7x,g0,/))') & - & ' fpm new NAME [[--lib|--src] [--app] [--test] [--example]]|[--full|--bare] [--backfill]' + & ' fpm new NAME [[--lib|--src] [--app] [--test] '//& + & ' [--example]]|[--full|--bare] [--backfill]' call fpm_stop(1,'directory name required') endif case(2) name=trim(unnamed(2)) case default write(stderr,'(7x,g0)') & - & ' fpm new NAME [[--lib|--src] [--app] [--test] [--example]]| [--full|--bare] [--backfill]' + & ' fpm new NAME [[--lib|--src] [--app] [--test] [--example]] '//& + & '| [--full|--bare] [--backfill]' call fpm_stop(2,'only one directory name allowed') end select !*! canon_path is not converting ".", etc. @@ -672,7 +680,8 @@ subroutine get_command_line_settings(cmd_settings) end if if (target_specific .and. any([skip, clean_all])) then - call fpm_stop(6, 'Cannot combine target-specific flags (--test, --apps, --examples) with --skip or --all.') + call fpm_stop(6, 'Cannot combine target-specific flags (--test, --apps, '//& + '--examples) with --skip or --all.') end if allocate(fpm_clean_settings :: cmd_settings) @@ -829,18 +838,20 @@ subroutine set_help() ' '] help_list_dash = [character(len=80) :: & ' ', & - ' build [--compiler COMPILER_NAME] [--profile PROF] [--flag FFLAGS] [--list] ', & - ' [--tests] [--no-prune] [--dump [FILENAME]] [--config-file PATH] ', & + ' build [--compiler COMPILER_NAME] [--profile PROF] [--features LIST] [--list] ', & + ' [--flag FFLAGS] [--tests] [--no-prune] [--dump [FILENAME]] ', & + ' [--config-file PATH] ', & ' help [NAME(s)] ', & ' new NAME [[--lib|--src] [--app] [--test] [--example]]| ', & ' [--full|--bare][--backfill] ', & ' update [NAME(s)] [--fetch-only] [--clean] [--verbose] [--dump [FILENAME]] ', & ' list [--list] ', & - ' run [[--target] NAME(s) [--example] [--profile PROF] [--flag FFLAGS] [--all] ', & + ' run [[--target] NAME(s) [--example] [--profile PROF] [--features LIST] [--all]', & ' [--runner "CMD"] [--compiler COMPILER_NAME] [--list] [-- ARGS] ', & - ' [--config-file PATH] ', & - ' test [[--target] NAME(s)] [--profile PROF] [--flag FFLAGS] [--runner "CMD"] ', & - ' [--list] [--compiler COMPILER_NAME] [--config-file PATH] [-- ARGS] ', & + ' [--config-file PATH] [--flag FFLAGS] ', & + ' test [[--target] NAME(s)] [--profile PROF] [--features LIST] [--flag FFLAGS] ', & + ' [--runner "CMD"] [--list] [--compiler COMPILER_NAME] [--config-file PATH] ', & + ' [-- ARGS] ', & ' install [--profile PROF] [--flag FFLAGS] [--no-rebuild] [--prefix PATH] ', & ' [--config-file PATH] [--registry-cache] [options] ', & ' clean [--skip|--all] [--test] [--apps] [--examples] [--config-file PATH] ', & @@ -1154,9 +1165,9 @@ subroutine set_help() ' build(1) - the fpm(1) subcommand to build a project ', & ' ', & 'SYNOPSIS ', & - ' fpm build [--profile PROF] [--flag FFLAGS] [--compiler COMPILER_NAME] ', & - ' [--build-dir DIR] [--list] [--tests] [--config-file PATH] ', & - ' [--dump [FILENAME]] ', & + ' fpm build [--profile PROF] [--features LIST] [--flag FFLAGS] ', & + ' [--compiler COMPILER_NAME] [--build-dir DIR] [--list] ', & + ' [--tests] [--config-file PATH] [--dump [FILENAME]] ', & ' ', & ' fpm build --help|--version ', & ' ', & @@ -1200,6 +1211,8 @@ subroutine set_help() ' ', & ' fpm build # build with debug options ', & ' fpm build --profile release # build with high optimization ', & + ' fpm build --features mpi,openmp # build with specific features ', & + ' fpm build --profile development # use feature profile from fpm.toml', & ' fpm build --build-dir /tmp/my_build # build to custom directory ', & '' ] @@ -1343,9 +1356,9 @@ subroutine set_help() ' test(1) - the fpm(1) subcommand to run project tests ', & ' ', & 'SYNOPSIS ', & - ' fpm test [[--target] NAME(s)] [--profile PROF] [--flag FFLAGS] ', & - ' [--compiler COMPILER_NAME ] [--runner "CMD"] [--list] ', & - ' [-- ARGS] [--config-file PATH] ', & + ' fpm test [[--target] NAME(s)] [--profile PROF] [--features LIST] ', & + ' [--flag FFLAGS] [--compiler COMPILER_NAME] [--runner "CMD"] ', & + ' [--list] [-- ARGS] [--config-file PATH] ', & ' ', & ' fpm test --help|--version ', & ' ', & @@ -1387,7 +1400,8 @@ subroutine set_help() ' # run a specific test and pass arguments to the command ', & ' fpm test mytest -- -x 10 -y 20 --title "my title line" ', & ' ', & - ' fpm test tst1 tst2 --profile PROF # run production version of two tests', & + ' fpm test tst1 tst2 --profile release # run release version of tests ', & + ' fpm test --features debug,mpi # run tests with specific features ', & '' ] help_update=[character(len=80) :: & 'NAME', & @@ -1606,6 +1620,7 @@ subroutine build_settings(self, list, show_model, build_tests, config_file) character(len=:), allocatable :: comp, ccomp, cxcomp, arch character(len=:), allocatable :: fflags, cflags, cxxflags, ldflags character(len=:), allocatable :: prof, cfg, dump, dir + character(len=:), allocatable :: feats ! Read CLI/env values (sget returns what set_args registered, including defaults) ! This is equivalent to check_build_vals @@ -1615,6 +1630,7 @@ subroutine build_settings(self, list, show_model, build_tests, config_file) cxxflags = ' ' // sget('cxx-flag') ldflags = ' ' // sget('link-flag') prof = sget('profile') + feats = sget('features') ! Set and validate build directory dir = sget('build-dir') @@ -1642,8 +1658,25 @@ subroutine build_settings(self, list, show_model, build_tests, config_file) cfg = sget('config-file') end if - ! Assign into this (polymorphic) object; allocatable chars auto-allocate - self%profile = prof + ! Validate mutually exclusive options + if (specified('profile') .and. specified('features')) then + call fpm_stop(1, 'Error: --profile and --features cannot be used together') + end if + + ! Parse comma-separated features and profiles + if (specified('features') .and. len_trim(feats) > 0) then + call parse_features(feats, self%features) + else + if (allocated(self%features)) deallocate(self%features) + end if + + if (specified('profile') .and. len_trim(prof) > 0) then + self%profile = prof + else + if (allocated(self%profile)) deallocate(self%profile) + end if + + ! Assign into this (polymorphic) object; allocatable chars auto-allocate self%prune = .not. lget('no-prune') self%compiler = comp self%c_compiler = ccomp @@ -1664,5 +1697,42 @@ subroutine build_settings(self, list, show_model, build_tests, config_file) if (present(build_tests)) self%build_tests = build_tests end subroutine build_settings + !> Parse comma-separated features string into string_t array + subroutine parse_features(features_str, features_array) + character(len=*), intent(in) :: features_str + type(string_t), allocatable, intent(out) :: features_array(:) + + character(len=:), allocatable :: trimmed_features(:) + integer :: i + + ! Split by comma + call split(features_str, trimmed_features, delimiters=',') + + ! Validate and clean feature names + if (size(trimmed_features) == 0) then + call fpm_stop(1, 'Error: Empty features list provided') + end if + + allocate(features_array(size(trimmed_features))) + + do i = 1, size(trimmed_features) + ! Trim whitespace + trimmed_features(i) = trim(adjustl(trimmed_features(i))) + + ! Validate feature name + if (len_trim(trimmed_features(i)) == 0) then + call fpm_stop(1, 'Error: Empty feature name in features list') + end if + + ! Check for valid feature name (similar to Fortran identifier rules) + if (.not. is_valid_feature_name(trimmed_features(i))) then + call fpm_stop(1, 'Error: Invalid feature name "'//trimmed_features(i)//'"') + end if + + features_array(i)%s = trimmed_features(i) + end do + + end subroutine parse_features + end module fpm_command_line diff --git a/src/fpm_compile_commands.F90 b/src/fpm_compile_commands.F90 index 826250aa30..57975c45d2 100644 --- a/src/fpm_compile_commands.F90 +++ b/src/fpm_compile_commands.F90 @@ -58,6 +58,12 @@ module fpm_compile_commands interface compile_command_t module procedure cct_new end interface compile_command_t + + !> Add compile commands to array (gcc-15 bug workaround) + interface add_compile_command + module procedure add_compile_command_one + module procedure add_compile_command_many + end interface add_compile_command contains @@ -330,7 +336,7 @@ pure subroutine cct_register_object(self, command, error) type(error_t), allocatable, intent(out) :: error if (allocated(self%command)) then - self%command = [self%command, command] + call add_compile_command(self%command, command) else allocate(self%command(1), source=command) end if @@ -442,6 +448,57 @@ logical function cct_is_same(this,that) !> All checks passed! cct_is_same = .true. - end function cct_is_same - + end function cct_is_same + + !> Add one compile command to array with a loop (gcc-15 bug on array initializer) + pure subroutine add_compile_command_one(list,new) + type(compile_command_t), allocatable, intent(inout) :: list(:) + type(compile_command_t), intent(in) :: new + + integer :: i,n + type(compile_command_t), allocatable :: tmp(:) + + if (allocated(list)) then + n = size(list) + else + n = 0 + end if + + allocate(tmp(n+1)) + do i=1,n + tmp(i) = list(i) + end do + tmp(n+1) = new + call move_alloc(from=tmp,to=list) + + end subroutine add_compile_command_one + + !> Add multiple compile commands to array with a loop (gcc-15 bug on array initializer) + pure subroutine add_compile_command_many(list,new) + type(compile_command_t), allocatable, intent(inout) :: list(:) + type(compile_command_t), intent(in) :: new(:) + + integer :: i,n,add + type(compile_command_t), allocatable :: tmp(:) + + if (allocated(list)) then + n = size(list) + else + n = 0 + end if + + add = size(new) + if (add == 0) return + + allocate(tmp(n+add)) + do i=1,n + tmp(i) = list(i) + end do + do i=1,add + tmp(n+i) = new(i) + end do + call move_alloc(from=tmp,to=list) + + end subroutine add_compile_command_many + end module fpm_compile_commands diff --git a/src/fpm_compiler.F90 b/src/fpm_compiler.F90 index e3a61cbef7..9c06fc911e 100644 --- a/src/fpm_compiler.F90 +++ b/src/fpm_compiler.F90 @@ -304,7 +304,7 @@ function get_default_flags(self, release) result(flags) select case (self%id) case (id_gcc, id_f95, id_caf, id_flang_classic, id_f18, id_lfortran, & id_intel_classic_nix, id_intel_classic_mac, id_intel_llvm_nix, & - id_pgi, id_nvhpc, id_nag, id_cray, id_ibmxl) + id_intel_llvm_unknown, id_pgi, id_nvhpc, id_nag, id_cray, id_ibmxl) pic_flag = " -fPIC" case (id_flang) ! LLVM Flang doesn't support -fPIC on Windows MSVC target diff --git a/src/fpm_sources.f90 b/src/fpm_sources.f90 index df95beceac..657f992abf 100644 --- a/src/fpm_sources.f90 +++ b/src/fpm_sources.f90 @@ -8,7 +8,7 @@ module fpm_sources use fpm_model, only: srcfile_t, FPM_UNIT_PROGRAM use fpm_filesystem, only: basename, canon_path, dirname, join_path, list_files, is_hidden_file use fpm_environment, only: get_os_type,OS_WINDOWS -use fpm_strings, only: lower, str_ends_with, string_t, operator(.in.) +use fpm_strings, only: lower, str_ends_with, string_t, operator(.in.), add_strings use fpm_source_parsing, only: parse_f_source, parse_c_source use fpm_manifest_executable, only: executable_config_t use fpm_manifest_preprocess, only: preprocess_config_t @@ -16,12 +16,18 @@ module fpm_sources private public :: add_sources_from_dir, add_executable_sources -public :: get_exe_name_with_suffix +public :: get_exe_name_with_suffix, add_srcfile character(4), parameter :: fortran_suffixes(2) = [".f90", & ".f "] character(4), parameter :: c_suffixes(4) = [".c ", ".h ", ".cpp", ".hpp"] +!> Add one or multiple source files to a source file array (gcc-15 bug workaround) +interface add_srcfile + module procedure add_srcfile_one + module procedure add_srcfile_many +end interface add_srcfile + contains !> Wrapper to source parsing routines. @@ -159,7 +165,7 @@ subroutine add_sources_from_dir(sources,directory,scope,with_executables,with_f_ if (.not.allocated(sources)) then sources = pack(dir_sources,.not.exclude_source) else - sources = [sources, pack(dir_sources,.not.exclude_source)] + call add_srcfile(sources, pack(dir_sources,.not.exclude_source)) end if end subroutine add_sources_from_dir @@ -239,7 +245,7 @@ subroutine add_executable_sources(sources,executables,scope,auto_discover,with_f if (.not.allocated(sources)) then sources = [exe_source] else - sources = [sources, exe_source] + call add_srcfile(sources, exe_source) end if end do exe_loop @@ -274,7 +280,7 @@ subroutine get_executable_source_dirs(exe_dirs,executables) if (.not.allocated(exe_dirs)) then exe_dirs = dirs_temp(1:n) else - exe_dirs = [exe_dirs,dirs_temp(1:n)] + call add_strings(exe_dirs,dirs_temp(1:n)) end if end subroutine get_executable_source_dirs @@ -296,4 +302,55 @@ function get_exe_name_with_suffix(source) result(suffixed) end function get_exe_name_with_suffix +!> Add one source file to a source file array with a loop (gcc-15 bug on array initializer) +pure subroutine add_srcfile_one(list,new) + type(srcfile_t), allocatable, intent(inout) :: list(:) + type(srcfile_t), intent(in) :: new + + integer :: i,n + type(srcfile_t), allocatable :: tmp(:) + + if (allocated(list)) then + n = size(list) + else + n = 0 + end if + + allocate(tmp(n+1)) + do i=1,n + tmp(i) = list(i) + end do + tmp(n+1) = new + call move_alloc(from=tmp,to=list) + +end subroutine add_srcfile_one + +!> Add multiple source files to a source file array with a loop (gcc-15 bug on array initializer) +pure subroutine add_srcfile_many(list,new) + type(srcfile_t), allocatable, intent(inout) :: list(:) + type(srcfile_t), intent(in) :: new(:) + + integer :: i,n,add + type(srcfile_t), allocatable :: tmp(:) + + if (allocated(list)) then + n = size(list) + else + n = 0 + end if + + add = size(new) + if (add == 0) return + + allocate(tmp(n+add)) + do i=1,n + tmp(i) = list(i) + end do + do i=1,add + tmp(n+i) = new(i) + end do + call move_alloc(from=tmp,to=list) + +end subroutine add_srcfile_many + end module fpm_sources diff --git a/src/fpm_strings.f90 b/src/fpm_strings.f90 index 9c1deb1d3f..ed36bf9093 100644 --- a/src/fpm_strings.f90 +++ b/src/fpm_strings.f90 @@ -43,7 +43,7 @@ module fpm_strings private public :: f_string, lower, upper, split, split_first_last, split_lines_first_last, str_ends_with, string_t, str_begins_with_str -public :: to_fortran_name, is_fortran_name, add_strings +public :: to_fortran_name, is_fortran_name, is_valid_feature_name, add_strings public :: string_array_contains, string_cat, len_trim, operator(.in.), fnv_1a public :: replace, resize, str, join, glob public :: notabs, dilate, remove_newline_characters, remove_characters_in_set @@ -1664,6 +1664,34 @@ function dilate(instr) result(outstr) end function dilate +!> Check if feature name is valid (alphanumeric, underscore, dash allowed) +logical function is_valid_feature_name(name) result(valid) + character(len=*), intent(in) :: name + integer :: i + character :: c + + valid = .false. + + ! Must not be empty and not too long + if (len_trim(name) == 0 .or. len_trim(name) > 64) return + + ! First character must be alphabetic or underscore + c = name(1:1) + if (.not. ((c >= 'a' .and. c <= 'z') .or. (c >= 'A' .and. c <= 'Z') .or. c == '_')) return + + ! Remaining characters can be alphanumeric, underscore, or dash + do i = 2, len_trim(name) + c = name(i:i) + if (.not. ((c >= 'a' .and. c <= 'z') .or. (c >= 'A' .and. c <= 'Z') .or. & + (c >= '0' .and. c <= '9') .or. c == '_' .or. c == '-')) then + return + end if + end do + + valid = .true. + +end function is_valid_feature_name + !> Add one element to a string array with a loop (gcc-15 bug on array initializer) pure subroutine add_strings_one(list,new) type(string_t), allocatable, intent(inout) :: list(:) diff --git a/src/fpm_targets.f90 b/src/fpm_targets.f90 index 04a10538e6..7c7435ef93 100644 --- a/src/fpm_targets.f90 +++ b/src/fpm_targets.f90 @@ -47,7 +47,7 @@ module fpm_targets FPM_TARGET_SHARED, FPM_TARGET_NAME public build_target_t, build_target_ptr public targets_from_sources, resolve_module_dependencies -public add_target, new_target, add_dependency, get_library_dirs +public add_target, new_target, add_dependency, get_library_dirs, add_target_ptr public filter_library_targets, filter_executable_targets, filter_modules @@ -156,6 +156,12 @@ module fpm_targets module procedure add_old_targets end interface +!> Add one or multiple build target pointers to array (gcc-15 bug workaround) +interface add_target_ptr + module procedure add_target_ptr_one + module procedure add_target_ptr_many +end interface add_target_ptr + contains !> Target type name @@ -699,7 +705,7 @@ subroutine add_old_targets(targets, add_targets) endassociate end do - targets = [targets, add_targets ] + call add_target_ptr(targets, add_targets) end subroutine add_old_targets @@ -723,7 +729,7 @@ subroutine add_dependency(target, dependency) end do if (dependency%output_name==target%output_name) return - target%dependencies = [target%dependencies, build_target_ptr(dependency)] + call add_target_ptr(target%dependencies, build_target_ptr(dependency)) end subroutine add_dependency @@ -1070,7 +1076,7 @@ end subroutine prune_build_targets subroutine resolve_target_linking(targets, model, library, error) type(build_target_ptr), intent(inout), target :: targets(:) type(fpm_model_t), intent(in) :: model - type(library_config_t), intent(in), optional :: library + type(library_config_t), intent(in), optional :: library type(error_t), allocatable, intent(out) :: error integer :: i,j @@ -1079,8 +1085,10 @@ subroutine resolve_target_linking(targets, model, library, error) character(:), allocatable :: global_link_flags, local_link_flags character(:), allocatable :: global_include_flags, shared_lib_paths + if (size(targets) == 0) return + global_link_flags = "" if (allocated(model%link_libraries)) then if (size(model%link_libraries) > 0) then @@ -1110,13 +1118,14 @@ subroutine resolve_target_linking(targets, model, library, error) associate(target => targets(i)%ptr) + ! If the main program is a C/C++ one, some compilers require additional linking flags, see ! https://stackoverflow.com/questions/36221612/p3dfft-compilation-ifort-compiler-error-multiple-definiton-of-main ! In this case, compile_flags were already allocated if (.not.allocated(target%compile_flags)) allocate(character(len=0) :: target%compile_flags) target%compile_flags = target%compile_flags//' ' - + select case (target%target_type) case (FPM_TARGET_C_OBJECT) target%compile_flags = target%compile_flags//model%c_compile_flags @@ -1135,7 +1144,8 @@ subroutine resolve_target_linking(targets, model, library, error) if (len(global_include_flags) > 0) then target%compile_flags = target%compile_flags//global_include_flags end if - + + call target%set_output_dir(get_output_dir(model%build_prefix, target%compile_flags)) end associate @@ -1247,12 +1257,12 @@ subroutine resolve_target_linking(targets, model, library, error) error=error, & exclude_self=.not.has_self_lib) - - end if + + ! On macOS, add room for 2 install_name_tool paths + target%link_flags = target%link_flags // model%compiler%get_headerpad_flags() ! On macOS, add room for 2 install_name_tool paths (always needed for executables) - target%link_flags = target%link_flags // model%compiler%get_headerpad_flags() if (allocated(target%link_libraries)) then if (size(target%link_libraries) > 0) then @@ -1545,4 +1555,55 @@ subroutine library_targets_to_deps(model, targets, target_ID) end subroutine library_targets_to_deps +!> Add one build target pointer to array with a loop (gcc-15 bug on array initializer) +subroutine add_target_ptr_one(list,new) + type(build_target_ptr), allocatable, intent(inout) :: list(:) + type(build_target_ptr), intent(in) :: new + + integer :: i,n + type(build_target_ptr), allocatable :: tmp(:) + + if (allocated(list)) then + n = size(list) + else + n = 0 + end if + + allocate(tmp(n+1)) + do i=1,n + tmp(i) = list(i) + end do + tmp(n+1) = new + call move_alloc(from=tmp,to=list) + +end subroutine add_target_ptr_one + +!> Add multiple build target pointers to array with a loop (gcc-15 bug on array initializer) +subroutine add_target_ptr_many(list,new) + type(build_target_ptr), allocatable, intent(inout) :: list(:) + type(build_target_ptr), intent(in) :: new(:) + + integer :: i,n,add + type(build_target_ptr), allocatable :: tmp(:) + + if (allocated(list)) then + n = size(list) + else + n = 0 + end if + + add = size(new) + if (add == 0) return + + allocate(tmp(n+add)) + do i=1,n + tmp(i) = list(i) + end do + do i=1,add + tmp(n+i) = new(i) + end do + call move_alloc(from=tmp,to=list) + +end subroutine add_target_ptr_many + end module fpm_targets diff --git a/src/metapackage/fpm_meta_base.f90 b/src/metapackage/fpm_meta_base.f90 index cef921d387..e069389760 100644 --- a/src/metapackage/fpm_meta_base.f90 +++ b/src/metapackage/fpm_meta_base.f90 @@ -69,6 +69,12 @@ module fpm_meta_base end type metapackage_t + !> Add dependencies to array (gcc-15 bug workaround) + interface add_dependency_config + module procedure add_dependency_config_one + module procedure add_dependency_config_many + end interface add_dependency_config + contains elemental subroutine destroy(this) @@ -161,7 +167,7 @@ subroutine resolve_package_config(self,package,error) ! as they may change if built upstream if (self%has_dependencies) then if (allocated(package%dev_dependency)) then - package%dev_dependency = [package%dev_dependency,self%dependency] + call add_dependency_config(package%dev_dependency,self%dependency) else package%dev_dependency = self%dependency end if @@ -234,4 +240,55 @@ end function dn end subroutine resolve_package_config + !> Add one dependency to array with a loop (gcc-15 bug on array initializer) + pure subroutine add_dependency_config_one(list,new) + type(dependency_config_t), allocatable, intent(inout) :: list(:) + type(dependency_config_t), intent(in) :: new + + integer :: i,n + type(dependency_config_t), allocatable :: tmp(:) + + if (allocated(list)) then + n = size(list) + else + n = 0 + end if + + allocate(tmp(n+1)) + do i=1,n + tmp(i) = list(i) + end do + tmp(n+1) = new + call move_alloc(from=tmp,to=list) + + end subroutine add_dependency_config_one + + !> Add multiple dependencies to array with a loop (gcc-15 bug on array initializer) + pure subroutine add_dependency_config_many(list,new) + type(dependency_config_t), allocatable, intent(inout) :: list(:) + type(dependency_config_t), intent(in) :: new(:) + + integer :: i,n,add + type(dependency_config_t), allocatable :: tmp(:) + + if (allocated(list)) then + n = size(list) + else + n = 0 + end if + + add = size(new) + if (add == 0) return + + allocate(tmp(n+add)) + do i=1,n + tmp(i) = list(i) + end do + do i=1,add + tmp(n+i) = new(i) + end do + call move_alloc(from=tmp,to=list) + + end subroutine add_dependency_config_many + end module fpm_meta_base diff --git a/test/cli_test/cli_test.f90 b/test/cli_test/cli_test.f90 index 581b89cafe..7df098b2cb 100644 --- a/test/cli_test/cli_test.f90 +++ b/test/cli_test/cli_test.f90 @@ -18,6 +18,7 @@ program main ! assuming no name over 15 characters to make output have shorter lines character(len=15),allocatable :: name(:),act_name(:) ; namelist/act_cli/act_name +character(len=15),allocatable :: features(:),act_features(:) ; namelist/act_cli/act_features integer,parameter :: max_names=10 character(len=:),allocatable :: command @@ -40,7 +41,7 @@ program main character(len=:), allocatable :: profile,act_profile ; namelist/act_cli/act_profile character(len=:), allocatable :: args,act_args ; namelist/act_cli/act_args -namelist/expected/cmd,cstat,estat,w_e,w_t,c_s,c_a,c_t,c_apps,c_ex,reg_c,name,profile,args,show_v,show_u_d,dry_run,token +namelist/expected/cmd,cstat,estat,w_e,w_t,c_s,c_a,c_t,c_apps,c_ex,reg_c,name,features,profile,args,show_v,show_u_d,dry_run,token integer :: lun logical,allocatable :: tally(:) logical,allocatable :: subtally(:) @@ -71,10 +72,13 @@ program main 'CMD="test proj1 p2 project3 --profile debug", NAME="proj1","p2","project3",profile="debug",', & 'CMD="test proj1 p2 project3 --profile release", NAME="proj1","p2","project3",profile="release",', & 'CMD="test proj1 p2 project3 --profile release -- arg1 -x ""and a long one""", & - &NAME="proj1","p2","project3",profile="release" ARGS="""arg1"" ""-x"" ""and a long one""", ', & + &NAME="proj1","p2","project3",profile="release",ARGS="""arg1"" ""-x"" ""and a long one""", ', & -'CMD="build", NAME=, profile="",ARGS="",', & -'CMD="build --profile release", NAME=, profile="release",ARGS="",', & +'CMD="build", NAME=, profile="",features=,ARGS="",', & +'CMD="build --profile release", NAME=, profile="release",features=,ARGS="",', & +'CMD="build --features debug,mpi", NAME=, profile="",features="debug","mpi",ARGS="",', & +'CMD="build --features single_feature", NAME=, profile="",features="single_feature",ARGS="",', & +'CMD="test --features debug,openmp", NAME=, profile="",features="debug","openmp",ARGS="",', & 'CMD="clean", NAME=, ARGS="",', & 'CMD="clean --skip", C_S=T, NAME=, ARGS="",', & @@ -115,6 +119,8 @@ program main endif ! blank out name group EXPECTED name=[(repeat(' ',len(name)),i=1,max_names)] ! the words on the command line sans the subcommand name + if(.not.allocated(features)) allocate(character(len=15) :: features(max_names)) + features=[(repeat(' ',15),i=1,max_names)] ! the features on the command line profile='' ! --profile PROF w_e=.false. ! --app w_t=.false. ! --test @@ -139,6 +145,8 @@ program main if(estat==0)then open(file='_test_cli',newunit=lun,delim='quote') act_name=[(repeat(' ',len(act_name)),i=1,max_names)] + if(.not.allocated(act_features)) allocate(character(len=15) :: act_features(max_names)) + act_features=[(repeat(' ',15),i=1,max_names)] act_profile='' act_w_e=.false. act_w_t=.false. @@ -158,6 +166,7 @@ program main ! compare results to expected values subtally=[logical ::] call test_test('NAME',all(act_name==name)) + call test_test('FEATURES',all(act_features==features)) call test_test('PROFILE',act_profile==profile) call test_test('SKIP',act_c_s.eqv.c_s) call test_test('ALL',act_c_a.eqv.c_a) @@ -250,6 +259,8 @@ subroutine parse() call get_command_line_settings(cmd_settings) allocate (character(len=len(name)) :: act_name(0) ) +allocate(character(len=15) :: act_features(max_names)) +act_features=[(repeat(' ',15),i=1,max_names)] act_args='' act_w_e=.false. act_w_t=.false. @@ -268,13 +279,28 @@ subroutine parse() act_w_t=settings%with_test act_name=[trim(settings%name)] type is (fpm_build_settings) - act_profile=settings%profile + if (allocated(settings%profile)) act_profile=settings%profile + if (allocated(settings%features)) then + do i = 1, min(size(settings%features),size(act_features)) + act_features(i) = settings%features(i)%s + end do + end if type is (fpm_run_settings) - act_profile=settings%profile + if (allocated(settings%profile)) act_profile=settings%profile + if (allocated(settings%features)) then + do i = 1, min(size(settings%features),size(act_features)) + act_features(i) = settings%features(i)%s + end do + end if act_name=settings%name if (allocated(settings%args)) act_args=settings%args type is (fpm_test_settings) - act_profile=settings%profile + if (allocated(settings%profile)) act_profile=settings%profile + if (allocated(settings%features)) then + do i = 1, min(size(settings%features),size(act_features)) + act_features(i) = settings%features(i)%s + end do + end if act_name=settings%name if (allocated(settings%args)) act_args=settings%args type is (fpm_clean_settings) diff --git a/test/fpm_test/test_features.f90 b/test/fpm_test/test_features.f90 index 8743517c4c..07509dbe00 100644 --- a/test/fpm_test/test_features.f90 +++ b/test/fpm_test/test_features.f90 @@ -39,7 +39,18 @@ subroutine collect_features(testsuite) & new_unittest("feature-extract-dependencies-examples", test_feature_extract_dependencies_examples), & & new_unittest("feature-extract-build-configs", test_feature_extract_build_configs), & & new_unittest("feature-extract-test-configs", test_feature_extract_test_configs), & - & new_unittest("feature-extract-example-configs", test_feature_extract_example_configs) & + & new_unittest("feature-extract-example-configs", test_feature_extract_example_configs), & + & new_unittest("dependency-feature-propagation", test_dependency_feature_propagation), & + & new_unittest("dependency-features-specification", test_dependency_features_specification), & + & new_unittest("feature-chained-os-commands", test_feature_chained_os_commands, should_fail=.true.), & + & new_unittest("feature-chained-compiler-commands", & + & test_feature_chained_compiler_commands, should_fail=.true.), & + & new_unittest("feature-complex-chain-compiler-os-compiler", & + & test_feature_complex_chain_compiler_os_compiler, should_fail=.true.), & + & new_unittest("feature-complex-chain-os-compiler-os", & + & test_feature_complex_chain_os_compiler_os, should_fail=.true.), & + & new_unittest("feature-mixed-valid-chains", test_feature_mixed_valid_chains), & + & new_unittest("feature-compiler-flags-integration", test_feature_compiler_flags_integration) & & ] end subroutine collect_features @@ -271,7 +282,8 @@ subroutine test_feature_collection_extract(error) ! Test extraction for gfortran on linux target_platform%compiler = id_gcc target_platform%os_type = OS_LINUX - extracted_feature = package%features(i)%extract_for_target(target_platform) + extracted_feature = package%features(i)%extract_for_target(target_platform, error=error) + if (allocated(error)) return ! Should have both base and gfortran-specific flags if (.not. allocated(extracted_feature%flags)) then @@ -288,7 +300,8 @@ subroutine test_feature_collection_extract(error) ! Test extraction for ifort target_platform%compiler = id_intel_classic_nix - extracted_feature = package%features(i)%extract_for_target(target_platform) + extracted_feature = package%features(i)%extract_for_target(target_platform, error=error) + if (allocated(error)) return if (.not. allocated(extracted_feature%flags)) then call test_failed(error, "Extracted ifort feature missing flags") @@ -458,7 +471,8 @@ subroutine test_feature_flag_addition(error) ! Test extraction for gfortran on linux (should get all three flags) target_platform%compiler = id_gcc target_platform%os_type = OS_LINUX - extracted_feature = package%features(i)%extract_for_target(target_platform) + extracted_feature = package%features(i)%extract_for_target(target_platform, error=error) + if (allocated(error)) return ! Should have flags from base, gfortran, and linux variants if (.not. allocated(extracted_feature%flags)) then @@ -530,7 +544,8 @@ subroutine test_feature_metapackage_addition(error) ! Test extraction for gfortran on linux (should get all three metapackages) target_platform%compiler = id_gcc target_platform%os_type = OS_LINUX - extracted_feature = package%features(i)%extract_for_target(target_platform) + extracted_feature = package%features(i)%extract_for_target(target_platform, error=error) + if (allocated(error)) return if (.not. package%features(i)%base%meta%openmp%on) then call test_failed(error, "Missing base openmp metapackage") @@ -604,7 +619,8 @@ subroutine test_feature_extract_gfortran_linux(error) do i = 1, size(package%features) if (package%features(i)%base%name == "debug") then target_platform = platform_config_t(id_gcc, OS_LINUX) - extracted_feature = package%features(i)%extract_for_target(target_platform) + extracted_feature = package%features(i)%extract_for_target(target_platform, error=error) + if (allocated(error)) return ! Check flags are combined correctly if (.not. allocated(extracted_feature%flags)) then @@ -688,7 +704,8 @@ subroutine test_feature_extract_ifort_windows(error) do i = 1, size(package%features) if (package%features(i)%base%name == "debug") then target_platform = platform_config_t("ifort", OS_WINDOWS) - extracted_feature = package%features(i)%extract_for_target(target_platform) + extracted_feature = package%features(i)%extract_for_target(target_platform, error=error) + if (allocated(error)) return ! Check flags: should have base + ifort + windows (but NOT linux) if (.not. allocated(extracted_feature%flags)) then @@ -781,7 +798,8 @@ subroutine test_feature_extract_dependencies_examples(error) do i = 1, size(package%features) if (package%features(i)%base%name == "testing") then target_platform = platform_config_t(id_gcc, OS_MACOS) - extracted_feature = package%features(i)%extract_for_target(target_platform) + extracted_feature = package%features(i)%extract_for_target(target_platform, error=error) + if (allocated(error)) return ! Check that all dependencies are combined (base + gfortran + macos) if (.not. allocated(extracted_feature%dependency)) then @@ -870,7 +888,8 @@ subroutine test_feature_extract_build_configs(error) do i = 1, size(package%features) if (package%features(i)%base%name == "optimization") then target_platform = platform_config_t(id_intel_classic_nix, OS_LINUX) - extracted_feature = package%features(i)%extract_for_target(target_platform) + extracted_feature = package%features(i)%extract_for_target(target_platform, error=error) + if (allocated(error)) return ! Check that build config is present if (.not. allocated(extracted_feature%build)) then @@ -952,7 +971,8 @@ subroutine test_feature_extract_test_configs(error) do i = 1, size(package%features) if (package%features(i)%base%name == "testing") then target_platform = platform_config_t(id_gcc, OS_WINDOWS) - extracted_feature = package%features(i)%extract_for_target(target_platform) + extracted_feature = package%features(i)%extract_for_target(target_platform, error=error) + if (allocated(error)) return ! Check that all test configs are combined (base + gfortran + windows + gfortran.windows) if (.not. allocated(extracted_feature%test) .or. size(extracted_feature%test) < 4) then @@ -1045,7 +1065,8 @@ subroutine test_feature_extract_example_configs(error) do i = 1, size(package%features) if (package%features(i)%base%name == "showcase") then target_platform = platform_config_t(id_intel_llvm_nix, OS_MACOS) - extracted_feature = package%features(i)%extract_for_target(target_platform) + extracted_feature = package%features(i)%extract_for_target(target_platform, error=error) + if (allocated(error)) return ! Check that all example configs are combined (base + ifx + macos + ifx.macos) if (.not. allocated(extracted_feature%example) .or. size(extracted_feature%example) < 4) then @@ -1097,4 +1118,489 @@ subroutine test_feature_extract_example_configs(error) end subroutine test_feature_extract_example_configs + !> Test that dependency features are correctly propagated and applied + subroutine test_dependency_feature_propagation(error) + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(package_config_t) :: dependency_config, exported_config + character(:), allocatable :: temp_file + integer :: unit + type(platform_config_t) :: target_platform + type(string_t), allocatable :: test_features(:) + + allocate(temp_file, source=get_temp_filename()) + + ! Create a dependency manifest with features + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'name = "test-dependency"', & + & 'version = "0.1.0"', & + & '[features]', & + & 'debug.flags = "-g -DDEBUG"', & + & 'debug.gfortran.flags = "-fcheck=bounds"', & + & 'mpi.flags = "-DUSE_MPI"', & + & 'mpi.dependencies.mpi = "*"', & + & '[[features.debug.executable]]', & + & 'name = "debug_tool"', & + & 'source-dir = "debug_tools"' + close(unit) + + ! Load the dependency configuration + call get_package_data(dependency_config, temp_file, error) + if (allocated(error)) return + + ! Simulate dependency requesting specific features (like dep%features from build_model) + allocate(test_features(2)) + test_features(1)%s = "debug" + test_features(2)%s = "mpi" + + ! Test export_config with these features (mimics line 132-133 in fpm.f90) + target_platform = platform_config_t(id_gcc, OS_LINUX) + exported_config = dependency_config%export_config(target_platform, test_features, error=error) + if (allocated(error)) return + + ! Verify that debug feature flags were applied + if (.not. allocated(exported_config%flags)) then + call test_failed(error, "Dependency export_config missing flags from debug feature") + return + end if + + if (index(exported_config%flags, "-g") == 0 .or. & + index(exported_config%flags, "-DDEBUG") == 0) then + call test_failed(error, "Dependency missing debug flags: got '" // exported_config%flags // "'") + return + end if + + if (index(exported_config%flags, "-fcheck=bounds") == 0) then + call test_failed(error, "Dependency missing gfortran-specific debug flags") + return + end if + + ! Verify that mpi feature flags were applied + if (index(exported_config%flags, "-DUSE_MPI") == 0) then + call test_failed(error, "Dependency missing mpi flags") + return + end if + + ! Verify that mpi metapackage was enabled + if (.not. exported_config%meta%mpi%on) then + call test_failed(error, "Dependency mpi metapackage not enabled") + return + end if + + ! Verify that debug executable was included + if (.not. allocated(exported_config%executable)) then + call test_failed(error, "Dependency debug executable not included") + return + end if + + if (size(exported_config%executable) < 1) then + call test_failed(error, "Dependency should have debug executable") + return + end if + + if (exported_config%executable(1)%name /= "debug_tool") then + call test_failed(error, "Dependency debug executable has wrong name") + return + end if + + end subroutine test_dependency_feature_propagation + + !> Test that main package can specify features for its dependencies + subroutine test_dependency_features_specification(error) + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(package_config_t) :: main_package + character(:), allocatable :: temp_file + integer :: unit, i + logical :: found_tomlf_dep + + allocate(temp_file, source=get_temp_filename()) + + ! Create a main package manifest that specifies features for dependencies + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'name = "main-package"', & + & 'version = "0.1.0"', & + & '[dependencies]', & + & '"dep-a" = { path = "../dep-a", features = ["openmp", "json"] }', & + & '"dep-b" = { path = "../dep-b", features = ["debug"] }' + close(unit) + + ! Load the main package configuration + call get_package_data(main_package, temp_file, error) + if (allocated(error)) return + + ! Verify dependencies were parsed correctly + if (.not. allocated(main_package%dependency)) then + call test_failed(error, "Main package dependencies not allocated") + return + end if + + if (size(main_package%dependency) /= 2) then + call test_failed(error, "Expected 2 dependencies, got " // & + char(size(main_package%dependency) + ichar('0'))) + return + end if + + ! Find and verify dep-a dependency with features + found_tomlf_dep = .false. + do i = 1, size(main_package%dependency) + if (main_package%dependency(i)%name == "dep-a") then + found_tomlf_dep = .true. + + ! Verify path configuration exists + if (.not. allocated(main_package%dependency(i)%path)) then + call test_failed(error, "dep-a dependency missing path configuration") + return + end if + + ! Path gets canonicalized, so just check it ends with the relative path + if (index(main_package%dependency(i)%path, "dep-a") == 0) then + call test_failed(error, "dep-a dependency path should contain 'dep-a', got: '" // & + main_package%dependency(i)%path // "'") + return + end if + + ! Verify features array - this is the key test + if (.not. allocated(main_package%dependency(i)%features)) then + call test_failed(error, "dep-a dependency features not allocated") + return + end if + + if (size(main_package%dependency(i)%features) /= 2) then + call test_failed(error, "dep-a dependency should have 2 features") + return + end if + + if (main_package%dependency(i)%features(1)%s /= "openmp" .or. & + main_package%dependency(i)%features(2)%s /= "json") then + call test_failed(error, "dep-a dependency has wrong feature names") + return + end if + exit + end if + end do + + if (.not. found_tomlf_dep) then + call test_failed(error, "dep-a dependency not found") + return + end if + + ! Verify dep-b dependency has features + do i = 1, size(main_package%dependency) + if (main_package%dependency(i)%name == "dep-b") then + if (.not. allocated(main_package%dependency(i)%features)) then + call test_failed(error, "dep-b dependency features not allocated") + return + end if + + if (size(main_package%dependency(i)%features) /= 1) then + call test_failed(error, "dep-b dependency should have 1 feature") + return + end if + + if (main_package%dependency(i)%features(1)%s /= "debug") then + call test_failed(error, "dep-b dependency has wrong feature name") + return + end if + exit + end if + end do + + end subroutine test_dependency_features_specification + + !> Test that chained OS commands are rejected (should fail) + subroutine test_feature_chained_os_commands(error) + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(package_config_t) :: package + character(:), allocatable :: temp_file + integer :: unit + + allocate(temp_file, source=get_temp_filename()) + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'name = "chained-os-test"', & + & 'version = "0.1.0"', & + & '[features]', & + & 'myfeature.windows.linux.flags = "-invalid"' ! Chained OS: windows.linux + close(unit) + + call get_package_data(package, temp_file, error) + + ! This should fail due to chained OS commands + + end subroutine test_feature_chained_os_commands + + !> Test that chained compiler commands are rejected (should fail) + subroutine test_feature_chained_compiler_commands(error) + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(package_config_t) :: package + character(:), allocatable :: temp_file + integer :: unit + + allocate(temp_file, source=get_temp_filename()) + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'name = "chained-compiler-test"', & + & 'version = "0.1.0"', & + & '[features]', & + & 'myfeature.gfortran.ifort.flags = "-invalid"' ! Chained compiler: gfortran.ifort + close(unit) + + call get_package_data(package, temp_file, error) + + ! This should fail due to chained compiler commands + + end subroutine test_feature_chained_compiler_commands + + !> Test complex chaining: compiler.os.compiler (should fail) + subroutine test_feature_complex_chain_compiler_os_compiler(error) + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(package_config_t) :: package + character(:), allocatable :: temp_file + integer :: unit + + allocate(temp_file, source=get_temp_filename()) + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'name = "complex-chain-test"', & + & 'version = "0.1.0"', & + & '[features]', & + & 'myfeature.gfortran.windows.ifort.flags = "-invalid"' ! gfortran.windows.ifort chain + close(unit) + + call get_package_data(package, temp_file, error) + + ! This should fail due to chained compiler constraints: gfortran -> windows (OK) -> ifort (ERROR) + + end subroutine test_feature_complex_chain_compiler_os_compiler + + !> Test complex chaining: os.compiler.os (should fail) + subroutine test_feature_complex_chain_os_compiler_os(error) + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(package_config_t) :: package + character(:), allocatable :: temp_file + integer :: unit + + allocate(temp_file, source=get_temp_filename()) + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'name = "complex-chain-test2"', & + & 'version = "0.1.0"', & + & '[features]', & + & 'myfeature.windows.ifx.macos.flags = "-invalid"' ! windows.ifx.macos chain + close(unit) + + call get_package_data(package, temp_file, error) + + ! This should fail due to chained OS constraints: windows -> ifx (OK) -> macos (ERROR) + + end subroutine test_feature_complex_chain_os_compiler_os + + !> Test mixed valid chains (should pass) + subroutine test_feature_mixed_valid_chains(error) + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(package_config_t) :: package + character(:), allocatable :: temp_file + integer :: unit + integer :: i + + allocate(temp_file, source=get_temp_filename()) + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'name = "valid-chains-test"', & + & 'version = "0.1.0"', & + & '[features]', & + & 'debug.flags = "-g"', & ! Base feature (all OS, all compilers) + & 'debug.gfortran.flags = "-Wall"', & ! Compiler-specific (gfortran, all OS) + & 'debug.windows.flags = "-DWINDOWS"', & ! OS-specific (all compilers, Windows) + & 'debug.gfortran.windows.flags = "-fbacktrace"', & ! Target-specific (gfortran + Windows) + & 'debug.linux.ifort.flags = "-check all"', & ! Target-specific (Linux + ifort) + & 'release.ifx.macos.flags = "-O3"' ! Another valid target-specific (ifx + macOS) + close(unit) + + call get_package_data(package, temp_file, error) + if (allocated(error)) return + + ! Verify that valid chains are accepted and collections created + if (.not. allocated(package%features)) then + call test_failed(error, "No feature collections found for valid chains test") + return + end if + + ! Should have debug and release features + if (size(package%features) < 2) then + call test_failed(error, "Expected at least 2 feature collections for valid chains") + return + end if + + ! Check that debug feature has multiple variants + do i = 1, size(package%features) + if (package%features(i)%base%name == "debug") then + if (.not. allocated(package%features(i)%variants)) then + call test_failed(error, "Debug collection should have variants for valid chains") + return + end if + + ! Should have multiple variants: gfortran, windows, gfortran.windows, linux.ifort + if (size(package%features(i)%variants) < 4) then + call test_failed(error, "Debug collection should have at least 4 variants for valid chains") + return + end if + exit + end if + end do + + end subroutine test_feature_mixed_valid_chains + + !> Test integration of feature compiler flags with new_compiler_flags + subroutine test_feature_compiler_flags_integration(error) + use fpm, only: new_compiler_flags + use fpm_model, only: fpm_model_t + use fpm_command_line, only: fpm_build_settings + use fpm_compiler, only: new_compiler, id_gcc + + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(package_config_t) :: package_config,package + type(fpm_model_t) :: model + type(fpm_build_settings) :: settings + type(platform_config_t) :: target_platform + character(:), allocatable :: temp_file + integer :: unit + + allocate(temp_file, source=get_temp_filename()) + + ! Create a test package with feature-based compiler flags + open(newunit=unit, file=temp_file, status='unknown') + write(unit, '(a)') 'name = "test_flags"' + write(unit, '(a)') 'version = "0.1.0"' + write(unit, '(a)') '' + write(unit, '(a)') '[library]' + write(unit, '(a)') 'source-dir = "src"' + write(unit, '(a)') '' + write(unit, '(a)') '[features]' + write(unit, '(a)') 'debug.gfortran.flags = "-g -Wall -fcheck=bounds"' + write(unit, '(a)') 'debug.flags = "-g"' + write(unit, '(a)') 'release.gfortran.flags = "-O3 -march=native"' + write(unit, '(a)') 'release.flags = "-O2"' + write(unit, '(a)') '' + write(unit, '(a)') '[profiles]' + write(unit, '(a)') 'development = ["debug"]' + write(unit, '(a)') 'production = ["release"]' + close(unit) + + ! Set up build settings without CLI flags + settings%flag = "" + settings%cflag = "" + settings%cxxflag = "" + settings%ldflag = "" + + ! Load the package configuration + call get_package_data(package_config, temp_file, error, apply_defaults=.true.) + if (allocated(error)) return + + ! 1) Choose first desired target platform: gfortran on Linux with development profile + target_platform = platform_config_t(id_gcc, OS_LINUX) + settings%profile = "development" ! This should activate debug features + + ! Extract the current package configuration request + package = package_config%export_config(target_platform, profile=settings%profile, error=error) + if (allocated(error)) return + + ! Set up model with mock compiler + call new_compiler(model%compiler, "gfortran", "gcc", "g++", echo=.false., verbose=.false.) + + ! Test that package flags are used when no CLI flags provided + call new_compiler_flags(model, settings, package) + + ! 2) Ensure flags are picked from gfortran platform (should include both base debug and gfortran-specific) + if (.not. allocated(model%fortran_compile_flags)) then + call test_failed(error, "Expected fortran_compile_flags to be allocated for gfortran") + return + end if + + if (index(model%fortran_compile_flags, "-g") == 0) then + call test_failed(error, "Expected debug flags to contain '-g' for gfortran platform") + return + end if + + if (index(model%fortran_compile_flags, "-Wall") == 0) then + call test_failed(error, "Expected gfortran-specific flags to contain '-Wall'") + return + end if + + if (index(model%fortran_compile_flags, "-fcheck=bounds") == 0) then + call test_failed(error, "Expected gfortran-specific flags to contain '-fcheck=bounds'") + return + end if + + ! 3) Choose another target platform: gfortran on Linux with production profile + settings%profile = "production" ! This should activate release features + + ! Extract the new package configuration request + package = package_config%export_config(target_platform, profile=settings%profile, error=error) + if (allocated(error)) return + + ! Reset flags and test production profile + call new_compiler_flags(model, settings, package) + + ! 4) Ensure flags are picked from the release platform (should include release flags) + if (.not. allocated(model%fortran_compile_flags)) then + call test_failed(error, "Expected fortran_compile_flags to be allocated for release") + return + end if + + if (index(model%fortran_compile_flags, "-O3") == 0) then + call test_failed(error, "Expected release gfortran flags to contain '-O3'") + return + end if + + if (index(model%fortran_compile_flags, "-march=native") == 0) then + call test_failed(error, "Expected release gfortran flags to contain '-march=native'") + return + end if + + if (index(model%fortran_compile_flags, "-O2") == 0) then + call test_failed(error, "Expected base release flags to contain '-O2'") + return + end if + + ! Test CLI flags still override package flags + settings%flag = "-O1 -DCUSTOM" + call new_compiler_flags(model, settings, package) + + if (index(model%fortran_compile_flags, "-O1") == 0) then + call test_failed(error, "Expected CLI flags to be used when provided") + return + end if + + if (index(model%fortran_compile_flags, "-DCUSTOM") == 0) then + call test_failed(error, "Expected CLI flags to contain custom flags") + return + end if + + ! Clean up - file was already closed after writing + + end subroutine test_feature_compiler_flags_integration + end module test_features diff --git a/test/fpm_test/test_manifest.f90 b/test/fpm_test/test_manifest.f90 index 36be44af82..64483f8c65 100644 --- a/test/fpm_test/test_manifest.f90 +++ b/test/fpm_test/test_manifest.f90 @@ -38,12 +38,14 @@ subroutine collect_manifest(testsuite) & new_unittest("dependency-invalid-git", test_dependency_invalid_git, should_fail=.true.), & & new_unittest("dependency-no-namespace", test_dependency_no_namespace, should_fail=.true.), & & new_unittest("dependency-redundant-v", test_dependency_redundant_v, should_fail=.true.), & + & new_unittest("dependency-features-present", test_dependency_features_present), & + & new_unittest("dependency-features-absent", test_dependency_features_absent), & + & new_unittest("dependency-features-empty", test_dependency_features_empty), & & new_unittest("dependency-wrongkey", test_dependency_wrongkey, should_fail=.true.), & & new_unittest("dependencies-empty", test_dependencies_empty), & & new_unittest("dependencies-typeerror", test_dependencies_typeerror, should_fail=.true.), & - ! FROZEN: Profile tests disabled during transition to feature-based architecture - ! & new_unittest("profiles", test_profiles), & - ! & new_unittest("profiles-keyvalue-table", test_profiles_keyvalue_table, should_fail=.true.), & + & new_unittest("profiles", test_profiles), & + & new_unittest("profiles-invalid", test_profiles_invalid, should_fail=.true.), & & new_unittest("executable-empty", test_executable_empty, should_fail=.true.), & & new_unittest("executable-typeerror", test_executable_typeerror, should_fail=.true.), & & new_unittest("executable-noname", test_executable_noname, should_fail=.true.), & @@ -81,7 +83,8 @@ subroutine collect_manifest(testsuite) & new_unittest("preprocessors-empty", test_preprocessors_empty, should_fail=.true.), & & new_unittest("macro-parsing", test_macro_parsing, should_fail=.false.), & & new_unittest("macro-parsing-dependency", & - & test_macro_parsing_dependency, should_fail=.false.) & + & test_macro_parsing_dependency, should_fail=.false.), & + & new_unittest("features-demo-serialization", test_features_demo_serialization) & & ] end subroutine collect_manifest @@ -535,10 +538,7 @@ subroutine test_dependencies_typeerror(error) end subroutine test_dependencies_typeerror - !> FROZEN TEST: Include a table of profiles in toml, check whether they are parsed correctly and stored in package - !> NOTE: This test is frozen during transition to feature-based architecture. - !> Profiles are now empty arrays, functionality moved to features. - !> Will be replaced with feature-based tests in future. + !> Test profile parsing and storage in package subroutine test_profiles(error) !> Error handling @@ -547,24 +547,18 @@ subroutine test_profiles(error) type(package_config_t) :: package character(len=*), parameter :: manifest = 'fpm-profiles.toml' integer :: unit - character(:), allocatable :: profile_name - logical :: profile_found - type(platform_config_t) :: target - type(profile_config_t) :: chosen_profile open(file=manifest, newunit=unit) write(unit, '(a)') & & 'name = "example"', & - & '[profiles.release.gfortran.linux]', & - & 'flags = "1" #release.gfortran.linux', & - & '[profiles.release.gfortran]', & - & 'flags = "2" #release.gfortran.all', & - & '[profiles.gfortran.linux]', & - & 'flags = "3" #all.gfortran.linux', & - & '[profiles.gfortran]', & - & 'flags = "4" #all.gfortran.all', & - & '[profiles.release.ifort]', & - & 'flags = "5" #release.ifort.all' + & '[profiles]', & + & 'development = ["debug", "testing"]', & + & 'release = ["optimized"]', & + & 'full-test = ["debug", "testing", "benchmarks"]', & + & '[features]', & + & 'testing.flags = " -g"', & + & 'optimized.flags = " -O2"', & + & 'benchmarks.flags = " -O3"' close(unit) call get_package_data(package, manifest, error) @@ -574,68 +568,39 @@ subroutine test_profiles(error) if (allocated(error)) return -! profile_name = 'release' -! compiler = 'gfortran' -! -! call find_profile(package%profiles, profile_name, compiler, 1, profile_found, chosen_profile) -! if (.not.(chosen_profile%flags().eq.'1 3')) then -! call test_failed(error, "Failed to append flags from profiles named 'all'") -! return -! end if -! -! call chosen_profile%test_serialization('profile serialization: '//profile_name//' '//compiler,error) -! if (allocated(error)) return -! -! profile_name = 'release' -! compiler = 'gfortran' -! call find_profile(package%profiles, profile_name, compiler, 3, profile_found, chosen_profile) -! if (.not.(chosen_profile%flags().eq.'2 4')) then -! call test_failed(error, "Failed to choose profile with OS 'all'") -! return -! end if -! -! call chosen_profile%test_serialization('profile serialization: '//profile_name//' '//compiler,error) -! if (allocated(error)) return -! -! profile_name = 'publish' -! compiler = 'gfortran' -! call find_profile(package%profiles, profile_name, compiler, 1, profile_found, chosen_profile) -! if (profile_found) then -! call test_failed(error, "Profile named "//profile_name//" should not exist") -! return -! end if -! -! call chosen_profile%test_serialization('profile serialization: '//profile_name//' '//compiler,error) -! if (allocated(error)) return -! -! profile_name = 'debug' -! compiler = 'ifort' -! call find_profile(package%profiles, profile_name, compiler, 3, profile_found, chosen_profile) -! if (.not.(chosen_profile%flags().eq.& -! ' /warn:all /check:all /error-limit:1 /Od /Z7 /assume:byterecl /traceback')) then -! call test_failed(error, "Failed to load built-in profile "//profile_name) -! return -! end if -! -! call chosen_profile%test_serialization('profile serialization: '//profile_name//' '//compiler,error) -! if (allocated(error)) return -! -! profile_name = 'release' -! compiler = 'ifort' -! call find_profile(package%profiles, profile_name, compiler, 1, profile_found, chosen_profile) -! if (.not.(chosen_profile%flags().eq.'5')) then -! call test_failed(error, "Failed to overwrite built-in profile") -! return -! end if - -! call chosen_profile%test_serialization('profile serialization: '//profile_name//' '//compiler,error) -! if (allocated(error)) return + ! Check that profiles were parsed correctly + if (.not. allocated(package%profiles)) then + call test_failed(error, "No profiles found in package") + return + end if + + ! debug, release, development, full-test + if (size(package%profiles) /= 4) then + call test_failed(error, "Unexpected number of profiles, should be 4") + return + end if + + ! Check development profile + if (package%profiles(1)%name /= "development") then + call test_failed(error, "Expected profile name 'development', got '" // package%profiles(1)%name // "'") + return + end if + + if (size(package%profiles(1)%features) /= 2) then + call test_failed(error, "Unexpected number of features, should be 2") + return + end if + + if (package%profiles(1)%features(1)%s /= "debug" .or. & + package%profiles(1)%features(2)%s /= "testing") then + call test_failed(error, "Incorrect features in development profile") + return + end if end subroutine test_profiles - !> FROZEN TEST: 'flags' is a key-value entry, test should fail as it is defined as a table - !> NOTE: This test is frozen during transition to feature-based architecture. - subroutine test_profiles_keyvalue_table(error) + !> Test invalid profile configuration should fail + subroutine test_profiles_invalid(error) !> Error handling type(error_t), allocatable, intent(out) :: error @@ -647,14 +612,15 @@ subroutine test_profiles_keyvalue_table(error) open(file=manifest, newunit=unit) write(unit, '(a)') & & 'name = "example"', & - & '[profiles.linux.flags]' + & '[profiles]', & + & 'development = "not_an_array"' close(unit) call get_package_data(package, manifest, error) open(file=manifest, newunit=unit) close(unit, status='delete') - end subroutine test_profiles_keyvalue_table + end subroutine test_profiles_invalid !> Executables cannot be created from empty tables subroutine test_executable_empty(error) @@ -1597,4 +1563,250 @@ subroutine test_macro_parsing_dependency(error) end subroutine test_macro_parsing_dependency + !> Ensure dependency "features" array is correctly parsed when present + subroutine test_dependency_features_present(error) + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(package_config_t) :: package + character(:), allocatable :: temp_file + integer :: unit, i, idx_dep0, idx_dep1, idx_dep2 + + allocate(temp_file, source=get_temp_filename()) + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'name = "example"', & + & 'version = "0.1.0"', & + & '[dependencies]', & + & '"dep0" = { path = "local/dep0", features = ["featA", "featB"] }', & + & '"dep1" = { git = "https://example.com/repo.git", tag = "v1.2.3", features = ["only"] }', & + & '"dep2" = { path = "other/dep2" }' + close(unit) + + call get_package_data(package, temp_file, error) + if (allocated(error)) return + + if (.not.allocated(package%dependency)) then + call test_failed(error, 'No dependencies parsed from manifest') + return + end if + + idx_dep0 = 0; idx_dep1 = 0; idx_dep2 = 0 + do i = 1, size(package%dependency) + select case (package%dependency(i)%name) + case ('dep0'); idx_dep0 = i + case ('dep1'); idx_dep1 = i + case ('dep2'); idx_dep2 = i + end select + end do + + if (idx_dep0 == 0 .or. idx_dep1 == 0 .or. idx_dep2 == 0) then + call test_failed(error, 'Expected dependencies dep0/dep1/dep2 not found') + return + end if + + ! dep0: features = ["featA","featB"] + if (.not.allocated(package%dependency(idx_dep0)%features)) then + call test_failed(error, 'dep0 features not allocated') + return + end if + if (size(package%dependency(idx_dep0)%features) /= 2) then + call test_failed(error, 'dep0 features size /= 2') + return + end if + if (package%dependency(idx_dep0)%features(1)%s /= 'featA' .or. & + & package%dependency(idx_dep0)%features(2)%s /= 'featB') then + call test_failed(error, 'dep0 features values mismatch') + return + end if + + ! dep1: features = ["only"] + if (.not.allocated(package%dependency(idx_dep1)%features)) then + call test_failed(error, 'dep1 features not allocated') + return + end if + if (size(package%dependency(idx_dep1)%features) /= 1) then + call test_failed(error, 'dep1 features size /= 1') + return + end if + if (package%dependency(idx_dep1)%features(1)%s /= 'only') then + call test_failed(error, 'dep1 features value mismatch') + return + end if + + ! dep2: no features key -> should be NOT allocated + if (allocated(package%dependency(idx_dep2)%features)) then + call test_failed(error, 'dep2 features should be unallocated when key is absent') + return + end if + end subroutine test_dependency_features_present + + + !> Ensure a dependency without "features" key is accepted (no allocation) + subroutine test_dependency_features_absent(error) + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(package_config_t) :: package + character(:), allocatable :: temp_file + integer :: unit, i + + allocate(temp_file, source=get_temp_filename()) + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'name = "example"', & + & '[dependencies]', & + & '"a" = { path = "a" }', & + & '"b" = { git = "https://example.org/b.git", branch = "main" }' + close(unit) + + call get_package_data(package, temp_file, error) + if (allocated(error)) return + + if (.not.allocated(package%dependency)) then + call test_failed(error, 'No dependencies parsed from manifest') + return + end if + + do i = 1, size(package%dependency) + if (allocated(package%dependency(i)%features)) then + call test_failed(error, 'features should be unallocated when not specified') + return + end if + end do + end subroutine test_dependency_features_absent + + + !> Accept an explicit empty "features = []" list + subroutine test_dependency_features_empty(error) + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(package_config_t) :: package + character(:), allocatable :: temp_file + integer :: unit, i, idx + + allocate(temp_file, source=get_temp_filename()) + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'name = "example"', & + & '[dependencies]', & + & '"empty" = { path = "local/empty", features = [] }' + close(unit) + + call get_package_data(package, temp_file, error) + if (allocated(error)) return + + idx = -1 + if (.not.allocated(package%dependency)) then + call test_failed(error, 'No dependencies parsed from manifest') + return + end if + + do i = 1, size(package%dependency) + if (package%dependency(i)%name == 'empty') then + idx = i + exit + end if + end do + + if (idx < 1) then + call test_failed(error, 'Dependency "empty" not found') + return + end if + + if (.not.allocated(package%dependency(idx)%features)) then + call test_failed(error, 'features should be allocated (size=0) for empty list') + return + end if + if (size(package%dependency(idx)%features) /= 0) then + call test_failed(error, 'features size should be zero for empty list') + return + end if + end subroutine test_dependency_features_empty + + !> Test features demo manifest serialization (from example_packages/features_demo/fpm.toml) + subroutine test_features_demo_serialization(error) + !> Error handling + type(error_t), allocatable, intent(out) :: error + + type(package_config_t) :: package + character(:), allocatable :: temp_file + integer :: unit + + allocate(temp_file, source=get_temp_filename()) + + open(file=temp_file, newunit=unit) + write(unit, '(a)') & + & 'name = "features_demo"', & + & 'version = "0.1.0"', & + & 'license = "MIT"', & + & 'description = "Demo package for FPM features functionality"', & + & '', & + & '[[executable]]', & + & 'name = "features_demo"', & + & 'source-dir = "app"', & + & 'main = "main.f90"', & + & '', & + & '[features]', & + & '# Base debug feature', & + & 'debug.flags = "-g"', & + & 'debug.preprocess.cpp.macros = "DEBUG"', & + & '', & + & '# Release feature', & + & 'release.flags = "-O3"', & + & 'release.preprocess.cpp.macros = "RELEASE"', & + & '', & + & '# Compiler-specific features', & + & 'debug.gfortran.flags = "-Wall -fcheck=bounds"', & + & 'release.gfortran.flags = "-march=native"', & + & '', & + & '# Platform-specific features', & + & 'linux.preprocess.cpp.macros = "LINUX_BUILD"', & + & '', & + & '# Parallel features', & + & 'mpi.preprocess.cpp.macros = "USE_MPI"', & + & 'mpi.dependencies.mpi = "*"', & + & 'openmp.preprocess.cpp.macros = "USE_OPENMP"', & + & 'openmp.dependencies.openmp = "*"', & + & '', & + & '[profiles]', & + & 'development = ["debug"]', & + & 'production = ["release", "openmp"]' + close(unit) + + call get_package_data(package, temp_file, error) + if (allocated(error)) return + + ! Verify basic package structure + if (package%name /= "features_demo") then + call test_failed(error, "Package name should be 'features_demo'") + return + end if + + if (.not. allocated(package%features)) then + call test_failed(error, "Features should be allocated") + return + end if + + if (.not. allocated(package%profiles)) then + call test_failed(error, "Profiles should be allocated") + return + end if + + if (.not. allocated(package%executable)) then + call test_failed(error, "Executables should be allocated") + return + end if + + ! Test package serialization roundtrip + call package%test_serialization('test_features_demo_serialization', error) + if (allocated(error)) return + + end subroutine test_features_demo_serialization + + end module test_manifest diff --git a/test/fpm_test/test_toml.f90 b/test/fpm_test/test_toml.f90 index e688e4a8c0..b31f6cce12 100644 --- a/test/fpm_test/test_toml.f90 +++ b/test/fpm_test/test_toml.f90 @@ -17,10 +17,10 @@ module test_toml use fpm_manifest_library, only: library_config_t use fpm_manifest_executable, only: executable_config_t use fpm_manifest_preprocess, only: preprocess_config_t - use fpm_manifest_profile, only: file_scope_flag use fpm_manifest_platform, only: platform_config_t use fpm_manifest_metapackages, only: metapackage_config_t use fpm_manifest_feature_collection, only: feature_collection_t + use fpm_manifest_profile, only: profile_config_t use fpm_environment, only: OS_ALL, OS_LINUX, OS_MACOS use fpm_versioning, only: new_version use fpm_strings, only: string_t, operator(==), split @@ -39,7 +39,6 @@ module test_toml contains - !> Collect all exported unit tests subroutine collect_toml(testsuite) @@ -64,7 +63,6 @@ subroutine collect_toml(testsuite) & new_unittest("serialize-library-config", library_config_roundtrip), & & new_unittest("serialize-executable-config", executable_config_roundtrip), & & new_unittest("serialize-preprocess-config", preprocess_config_roundtrip), & - & new_unittest("serialize-file-scope-flag", file_scope_flag_roundtrip), & & new_unittest("serialize-string-array", string_array_roundtrip), & & new_unittest("serialize-fortran-features", fft_roundtrip), & & new_unittest("serialize-fortran-invalid", fft_invalid, should_fail=.true.), & @@ -78,7 +76,8 @@ subroutine collect_toml(testsuite) & new_unittest("serialize-model", fpm_model_roundtrip), & & new_unittest("serialize-model-invalid", fpm_model_invalid, should_fail=.true.), & & new_unittest("serialize-metapackage-config", metapackage_config_roundtrip), & - & new_unittest("serialize-feature-collection", feature_collection_roundtrip)] + & new_unittest("serialize-feature-collection", feature_collection_roundtrip), & + & new_unittest("serialize-profile-config", profile_config_roundtrip)] end subroutine collect_toml @@ -1288,23 +1287,6 @@ subroutine preprocess_config_roundtrip(error) end subroutine preprocess_config_roundtrip - subroutine file_scope_flag_roundtrip(error) - - !> Error handling - type(error_t), allocatable, intent(out) :: error - - type(file_scope_flag) :: ff - - call ff%test_serialization('file_scope_flag: empty', error) - if (allocated(error)) return - - ff%file_name = "preprocessor config" - ff%flags = "-1 -f -2 -g" - - call ff%test_serialization('file_scope_flag: non-empty', error) - - end subroutine file_scope_flag_roundtrip - !> Test a metapackage configuration subroutine metapackage_config_roundtrip(error) @@ -1350,5 +1332,23 @@ subroutine feature_collection_roundtrip(error) end subroutine feature_collection_roundtrip + subroutine profile_config_roundtrip(error) + type(error_t), allocatable, intent(out) :: error + type(profile_config_t) :: profile + + ! Set up a profile with features + profile%name = "development" + + ! Allocate and populate features array + allocate(profile%features(3)) + profile%features(1)%s = "debug" + profile%features(2)%s = "testing" + profile%features(3)%s = "verbose" + + ! Round-trip via the generic serialization tester + call profile%test_serialization('profile_config_t: development profile', error) + + end subroutine profile_config_roundtrip + end module test_toml