diff --git a/src/fpm/installer.f90 b/src/fpm/installer.f90 index 1a8a98ee94..0132093da8 100644 --- a/src/fpm/installer.f90 +++ b/src/fpm/installer.f90 @@ -9,7 +9,7 @@ module fpm_installer use fpm_error, only : error_t, fatal_error use fpm_targets, only: build_target_t, FPM_TARGET_ARCHIVE, FPM_TARGET_SHARED, FPM_TARGET_NAME use fpm_filesystem, only : join_path, mkdir, exists, unix_path, windows_path, get_local_prefix, & - basename + basename, set_executable implicit none private @@ -210,11 +210,13 @@ subroutine install_executable(self, executable, error) call self%install(executable, self%bindir, error) if (allocated(error)) return + ! Set executable permissions on the installed file + exe_path = join_path(self%install_destination(self%bindir), basename(executable)) + call set_executable(exe_path) + ! on MacOS, add two relative paths for search of dynamic library dependencies: add_rpath: if (self%os==OS_MACOS) then - exe_path = join_path(self%install_destination(self%bindir) , basename(executable)) - ! First path: for bin/lib/include structure cmd = "install_name_tool -add_rpath @executable_path/../lib " // exe_path call self%run(cmd, error) diff --git a/src/fpm_filesystem.F90 b/src/fpm_filesystem.F90 index 6b22f498f9..80809c3cd1 100644 --- a/src/fpm_filesystem.F90 +++ b/src/fpm_filesystem.F90 @@ -16,7 +16,8 @@ module fpm_filesystem public :: basename, canon_path, dirname, is_dir, join_path, number_of_rows, list_files, get_local_prefix, & mkdir, exists, get_temp_filename, windows_path, unix_path, getline, delete_file, fileopen, fileclose, & filewrite, warnwrite, parent_dir, is_hidden_file, read_lines, read_lines_expanded, which, run, & - os_delete_dir, is_absolute_path, get_home, execute_and_read_output, get_dos_path + os_delete_dir, is_absolute_path, get_home, execute_and_read_output, get_dos_path, & + set_executable #ifndef FPM_BOOTSTRAP interface @@ -26,6 +27,13 @@ function c_opendir(dir) result(r) bind(c, name="c_opendir") type(c_ptr) :: r end function c_opendir + function c_chmod(path, mode) result(r) bind(c, name="chmod") + import c_char, c_int + character(kind=c_char), intent(in) :: path(*) + integer(kind=c_int), value :: mode + integer(kind=c_int) :: r + end function c_chmod + function c_readdir(dir) result(r) bind(c, name="c_readdir") import c_ptr type(c_ptr), intent(in), value :: dir @@ -1254,4 +1262,28 @@ function get_dos_path(path,error) end function get_dos_path + !> Set executable permissions (chmod +x on Unix, no-op on Windows) + subroutine set_executable(filename) + character(len=*), intent(in) :: filename + integer :: stat + + if (.not. os_is_unix()) return + +#ifndef FPM_BOOTSTRAP + ! Use C library chmod (faster, standard) + ! o'755' is octal for rwxr-xr-x + stat = c_chmod(filename // c_null_char, int(o'755', c_int)) + if (stat /= 0) then + write(stderr, *) "Warning: Failed to set executable permissions on: ", filename + end if +#else + ! Fallback using shell command + call run("chmod +x " // filename, echo=.false., verbose=.false., exitstat=stat) + if (stat /= 0) then + write(stderr, *) "Warning: Failed to set executable permissions on: ", filename + end if +#endif + + end subroutine set_executable + end module fpm_filesystem diff --git a/test/fpm_test/test_filesystem.f90 b/test/fpm_test/test_filesystem.f90 index 4c8d499bbf..2a4ad94234 100644 --- a/test/fpm_test/test_filesystem.f90 +++ b/test/fpm_test/test_filesystem.f90 @@ -2,7 +2,8 @@ module test_filesystem use testsuite, only: new_unittest, unittest_t, error_t, test_failed use fpm_filesystem, only: canon_path, is_dir, mkdir, os_delete_dir, & join_path, is_absolute_path, get_home, & - delete_file, read_lines, get_temp_filename + delete_file, read_lines, get_temp_filename, & + set_executable, filewrite, exists use fpm_environment, only: OS_WINDOWS, get_os_type, os_is_unix use fpm_strings, only: string_t, split_lines_first_last implicit none @@ -24,7 +25,8 @@ subroutine collect_filesystem(tests) & new_unittest("test-is-absolute-path", test_is_absolute_path), & & new_unittest("test-get-home", test_get_home), & & new_unittest("test-split-lines-first-last", test_split_lines_first_last), & - & new_unittest("test-crlf-lines", test_dir_with_crlf) & + & new_unittest("test-crlf-lines", test_dir_with_crlf), & + & new_unittest("test-set-executable", test_set_executable) & ] end subroutine collect_filesystem @@ -428,6 +430,41 @@ subroutine test_dir_with_crlf(error) 1 format("Failed reading file with CRLF: ",a,:,i0,:,a,:,i0) end subroutine test_dir_with_crlf + + ! Test for set_executable + subroutine test_set_executable(error) + type(error_t), allocatable, intent(out) :: error + character(len=:), allocatable :: temp_file + integer :: stat + + ! 1. Setup: Create a dummy file + temp_file = "test_exec_perm.txt" + call filewrite(temp_file, [character(len=13) :: "dummy content"]) + + if (.not. exists(temp_file)) then + call test_failed(error, "Setup failed: Could not create temp file") + return + end if + + ! 2. Action: Call the function + call set_executable(temp_file) + + ! 3. Assertion: Verify permissions + if (os_is_unix()) then + ! 'test -x' returns 0 if executable, 1 if not + call execute_command_line("test -x " // temp_file, exitstat=stat) + + if (stat /= 0) then + call test_failed(error, "Test Failed: File was not made executable on Unix") + ! Attempt cleanup before returning + call delete_file(temp_file) + return + end if + end if + + call delete_file(temp_file) + + end subroutine test_set_executable end module test_filesystem