Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/fpm/installer.f90
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
34 changes: 33 additions & 1 deletion src/fpm_filesystem.F90
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bootstrap fallback path silently ignores chmod failures by not checking the exitstat value. While a warning is logged in the non-bootstrap path when chmod fails, the bootstrap path should also handle failures consistently. Consider checking the stat variable and logging a warning if the command fails.

Suggested change
call run("chmod +x " // filename, echo=.false., verbose=.false., exitstat=stat)
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

Copilot uses AI. Check for mistakes.
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
41 changes: 39 additions & 2 deletions test/fpm_test/test_filesystem.f90
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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