Skip to content

feat: shared library targets #1138

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
7918b56
add `shared` option to the [library] manifest
perazz May 10, 2025
a3dfe13
generalize library name
perazz May 10, 2025
c71301c
on shared library, add per-project library files
perazz May 10, 2025
64257f6
build target: `info` pretty print
perazz May 10, 2025
d5a1314
fix library targets
perazz May 10, 2025
3e83987
exclude empty library files
perazz May 10, 2025
0064295
dependency: add dependency graph to `dependency_node_t`
perazz May 10, 2025
7623c4e
return all dependencies in a manifest
perazz May 10, 2025
cedcf3b
build dependency graph from the manifest
perazz May 10, 2025
faa01a4
remove `package_tmp`
perazz May 11, 2025
b92687b
recurse package dependencies
perazz May 11, 2025
9cf5c7d
localized topological sort for determining the linking order
perazz May 11, 2025
7e78d08
move `library_filename` to fpm_environment
perazz May 11, 2025
7fc19bf
fix order of the link dependencies
perazz May 12, 2025
b374bcb
enumerate shared libraries
perazz May 12, 2025
2ddf082
shared library target linking command
perazz May 12, 2025
5446a2c
compile all libraries separately
perazz May 12, 2025
113ec21
install all lirbary targets
perazz May 12, 2025
fa63aed
[cli] add comments to `fpm new`
perazz May 12, 2025
dd5526b
test: shared lib dependencies
perazz May 12, 2025
fde74e5
test: shared library installer
perazz May 12, 2025
7e7ad82
cleanup
perazz May 12, 2025
32f2aa5
BUGFIX: dev_dependency
perazz May 12, 2025
c27c282
[BUGFIX] ensure target libraries have dependencies among them to ensu…
perazz May 12, 2025
cffc325
add example: shared library with dependencies
perazz May 12, 2025
5beebcd
add shared library examples
perazz May 12, 2025
8c4bfe7
[run] cleanup runner command
perazz May 12, 2025
e6217ed
split get_library_dirs
perazz May 12, 2025
4f4d970
[run] add local library paths to the local environment to ruyn with d…
perazz May 12, 2025
8579677
always run position independent code
perazz May 12, 2025
8bffd89
fix
perazz May 12, 2025
414f448
`requires` (int) -> `package_dep` (string_t)
perazz May 12, 2025
d5f1ecf
do not prune source files when building shared libraries
perazz May 12, 2025
5bf09ea
more shared library tests
perazz May 12, 2025
6e4ff09
Delete .fpm_model.toml.swp
perazz May 12, 2025
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ build/*
# CodeBlocks
project/

# Temporary files
*.swp

16 changes: 16 additions & 0 deletions ci/run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -310,5 +310,21 @@ fi

popd

# Test shared library dependencies
pushd shared_lib
"$fpm" build || EXIT_CODE=$?
test $EXIT_CODE -eq 0
popd

pushd shared_lib_extra
"$fpm" build || EXIT_CODE=$?
test $EXIT_CODE -eq 0
popd

pushd shared_app_only
"$fpm" test || EXIT_CODE=$?
test $EXIT_CODE -eq 0
popd

# Cleanup
rm -rf ./*/build
4 changes: 4 additions & 0 deletions example_packages/shared_app_only/app/main.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
program main
use testdrive
print *, 'Hello, world!'
end program main
7 changes: 7 additions & 0 deletions example_packages/shared_app_only/fpm.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# App only, use shared lib from other folder
name = "shared_app_only"
library.shared=true
[dependencies]
shared_lib_extra = { path = "../shared_lib_extra" }
[dev-dependencies]
test-drive = { git = "https://github.com/fortran-lang/test-drive", tag="v0.5.0" }
50 changes: 50 additions & 0 deletions example_packages/shared_app_only/test/test.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
module test_shared_lib
use testdrive, only : new_unittest, unittest_type, error_type, check
use shared_lib, only: test_something

implicit none

public :: collect


contains

!> Collect all exported unit tests
subroutine collect(testsuite)
!> Collection of tests
type(unittest_type), allocatable, intent(out) :: testsuite(:)

testsuite = [ new_unittest("shared_lib", test_shared) ]

end subroutine collect

subroutine test_shared(error)
type(error_type), allocatable, intent(out) :: error

call check(error, test_something(), 123, "Should be test_something==123")

end subroutine test_shared

end module test_shared_lib

program tester
use, intrinsic :: iso_fortran_env, only : error_unit
use testdrive, only : run_testsuite, new_testsuite, testsuite_type
use test_shared_lib, only : collect
implicit none
integer :: stat
type(testsuite_type), allocatable :: testsuite
character(len=*), parameter :: fmt = '("#", *(1x, a))'

stat = 0

testsuite = new_testsuite("shared_lib", collect)

write(error_unit, fmt) "Testing:", testsuite%name
call run_testsuite(testsuite%collect, error_unit, stat)

if (stat > 0) then
write(error_unit, '(i0, 1x, a)') stat, "test(s) failed!"
error stop
end if
end program tester
3 changes: 3 additions & 0 deletions example_packages/shared_lib/fpm.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Shared library with no executables
name = "shared_lib"
library.shared=true
14 changes: 14 additions & 0 deletions example_packages/shared_lib/src/shared_lib.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module shared_lib
implicit none
private

public :: say_hello
public :: test_something
contains
subroutine say_hello
print *, "Hello, shared_lib!"
end subroutine say_hello
integer function test_something()
test_something = 123
end function test_something
end module shared_lib
4 changes: 4 additions & 0 deletions example_packages/shared_lib_extra/fpm.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
name = "shared_lib_extra"
library.shared=true
[dependencies]
shared_lib = { path = "../shared_lib" }
10 changes: 10 additions & 0 deletions example_packages/shared_lib_extra/src/shared_lib_extra.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module shared_lib_extra
implicit none
private

public :: say_extra_hello
contains
subroutine say_extra_hello
print *, "Hello, shared_lib_extra!"
end subroutine say_extra_hello
end module shared_lib_extra
132 changes: 108 additions & 24 deletions src/fpm.f90
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ module fpm

use fpm_sources, only: add_executable_sources, add_sources_from_dir
use fpm_targets, only: targets_from_sources, build_target_t, build_target_ptr, &
FPM_TARGET_EXECUTABLE, FPM_TARGET_ARCHIVE
FPM_TARGET_EXECUTABLE, get_library_dirs
use fpm_manifest, only : get_package_data, package_config_t
use fpm_meta, only : resolve_metapackages
use fpm_error, only : error_t, fatal_error, fpm_stop
Expand All @@ -26,7 +26,7 @@ module fpm
& stdout => output_unit, &
& stderr => error_unit
use iso_c_binding, only: c_char, c_ptr, c_int, c_null_char, c_associated, c_f_pointer
use fpm_environment, only: os_is_unix
use fpm_environment, only: os_is_unix, get_os_type, OS_WINDOWS, OS_MACOS, get_env, set_env, delete_env
use fpm_settings, only: fpm_global_settings, get_global_settings

implicit none
Expand Down Expand Up @@ -68,10 +68,10 @@ subroutine build_model(model, settings, package, error)
end if

call new_compiler_flags(model,settings)
model%build_prefix = join_path("build", basename(model%compiler%fc))
model%include_tests = settings%build_tests
model%build_prefix = join_path("build", basename(model%compiler%fc))
model%include_tests = settings%build_tests
model%enforce_module_names = package%build%module_naming
model%module_prefix = package%build%module_prefix
model%module_prefix = package%build%module_prefix

! Resolve meta-dependencies into the package and the model
call resolve_metapackages(model,package,settings,error)
Expand Down Expand Up @@ -447,7 +447,7 @@ subroutine cmd_build(settings)
call fpm_stop(1,'*cmd_build* Model error: '//error%message)
end if

call targets_from_sources(targets, model, settings%prune, error)
call targets_from_sources(targets, model, settings%prune, package%library, error)
if (allocated(error)) then
call fpm_stop(1,'*cmd_build* Target error: '//error%message)
end if
Expand Down Expand Up @@ -487,7 +487,7 @@ subroutine cmd_run(settings,test)
type(srcfile_t), pointer :: exe_source
integer :: run_scope,firsterror
integer, allocatable :: stat(:),target_ID(:)
character(len=:),allocatable :: line
character(len=:),allocatable :: line,run_cmd,library_path

call get_package_data(package, "fpm.toml", error, apply_defaults=.true.)
if (allocated(error)) then
Expand All @@ -499,7 +499,7 @@ subroutine cmd_run(settings,test)
call fpm_stop(1, '*cmd_run* Model error: '//error%message)
end if

call targets_from_sources(targets, model, settings%prune, error)
call targets_from_sources(targets, model, settings%prune, package%library, error)
if (allocated(error)) then
call fpm_stop(1, '*cmd_run* Targets error: '//error%message)
end if
Expand Down Expand Up @@ -581,25 +581,23 @@ subroutine cmd_run(settings,test)
call compact_list()
else

! Save current library path and set a new one that includes the local
! dynamic library folders
library_path = save_library_path()
call set_library_path(model, targets, error)
if (allocated(error)) call fpm_stop(1, '*cmd_run* Run error: '//error%message)

allocate(stat(size(executables)))
do i=1,size(executables)
if (exists(executables(i)%s)) then
if(settings%runner /= ' ')then
if(.not.allocated(settings%args))then
call run(settings%runner_command()//' '//executables(i)%s, &
echo=settings%verbose, exitstat=stat(i))
else
call run(settings%runner_command()//' '//executables(i)%s//" "//settings%args, &
echo=settings%verbose, exitstat=stat(i))
endif
else
if(.not.allocated(settings%args))then
call run(executables(i)%s,echo=settings%verbose, exitstat=stat(i))
else
call run(executables(i)%s//" "//settings%args,echo=settings%verbose, &
exitstat=stat(i))
endif
endif

! Prepare command line
run_cmd = executables(i)%s
if (settings%runner/=' ') run_cmd = settings%runner_command()//' '//run_cmd
if (allocated(settings%args)) run_cmd = run_cmd//" "//settings%args

call run(run_cmd,echo=settings%verbose,exitstat=stat(i))

else
call fpm_stop(1,'*cmd_run*:'//executables(i)%s//' not found')
end if
Expand All @@ -615,6 +613,10 @@ subroutine cmd_run(settings,test)
firsterror = findloc(stat/=0,value=.true.,dim=1)
call fpm_stop(stat(firsterror),'*cmd_run*:stopping due to failed executions')
end if

! Restore original library path
call restore_library_path(library_path, error)
if (allocated(error)) call fpm_stop(1, '*cmd_run* Environment error: '//error%message)

end if

Expand Down Expand Up @@ -793,4 +795,86 @@ logical function should_be_run(settings,run_scope,exe_target)

end function should_be_run

!> Save the current runtime library path (e.g., PATH or LD_LIBRARY_PATH)
function save_library_path() result(path)
character(len=:), allocatable :: path

select case (get_os_type())
case (OS_WINDOWS)
path = get_env("PATH", default="")
case (OS_MACOS)
! macOS does not use LD_LIBRARY_PATH by default for `.dylib`
allocate(character(0) :: path)
case default ! UNIX/Linux
path = get_env("LD_LIBRARY_PATH", default="")
end select
end function save_library_path

!> Set the runtime library path for the current process (used for subprocesses)
subroutine set_library_path(model, targets, error)
type(fpm_model_t), intent(in) :: model
type(build_target_ptr), intent(inout), target :: targets(:)
type(error_t), allocatable, intent(out) :: error

type(string_t), allocatable :: shared_lib_dirs(:)
character(len=:), allocatable :: new_path, sep
logical :: success
integer :: i

! Get library directories
call get_library_dirs(model, targets, shared_lib_dirs)

! Select platform-specific separator
select case (get_os_type())
case (OS_WINDOWS)
sep = ";"
case default
sep = ":"
end select

! Join the directories into a path string
! Manually join paths
new_path = ""
do i = 1, size(shared_lib_dirs)
if (i > 1) new_path = new_path // sep
new_path = new_path // shared_lib_dirs(i)%s
end do

! Set the appropriate environment variable
select case (get_os_type())
case (OS_WINDOWS)
success = set_env("PATH", new_path // sep // get_env("PATH", default=""))
case (OS_MACOS)
! Typically not required for local .dylib use, noop or DYLD_LIBRARY_PATH if needed
success = .true.
case default ! UNIX/Linux
success = set_env("LD_LIBRARY_PATH", new_path // sep // get_env("LD_LIBRARY_PATH", default=""))
end select

if (.not.success) call fatal_error(error," Cannot set library path: "//new_path)

end subroutine set_library_path

!> Restore a previously saved runtime library path
subroutine restore_library_path(saved_path,error)
character(*), intent(in) :: saved_path
type(error_t), allocatable, intent(out) :: error
logical :: success

select case (get_os_type())
case (OS_WINDOWS)
success = set_env("PATH", saved_path)
case (OS_MACOS)
! noop
success = .true.
case default ! UNIX/Linux
success = set_env("LD_LIBRARY_PATH", saved_path)
end select

if (.not.success) call fatal_error(error, "Cannot restore library path "//saved_path)

end subroutine restore_library_path



end module fpm
10 changes: 6 additions & 4 deletions src/fpm/cmd/install.f90
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ subroutine cmd_install(settings)
type(installer_t) :: installer
type(string_t), allocatable :: list(:)
logical :: installable
integer :: ntargets
integer :: ntargets,i

call get_package_data(package, "fpm.toml", error, apply_defaults=.true.)
call handle_error(error)

call build_model(model, settings, package, error)
call handle_error(error)

call targets_from_sources(targets, model, settings%prune, error)
call targets_from_sources(targets, model, settings%prune, package%library, error)
call handle_error(error)

call install_info(output_unit, settings%list, targets, ntargets)
Expand All @@ -65,8 +65,10 @@ subroutine cmd_install(settings)
call filter_library_targets(targets, list)

if (size(list) > 0) then
call installer%install_library(list(1)%s, error)
call handle_error(error)
do i=1,size(list)
call installer%install_library(list(i)%s, error)
call handle_error(error)
end do

call install_module_files(installer, targets, error)
call handle_error(error)
Expand Down
18 changes: 18 additions & 0 deletions src/fpm/cmd/new.f90
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,10 @@ subroutine cmd_new(settings)
&' # files and library archive. Without this being set to "true" an "install" ',&
&' # subcommand ignores parameters that specify library installation. ',&
&' ',&
&' # If your project is a shared library (see `[library] shared=true`), enabling ',&
&' # this will install the compiled `.so`, `.dylib`, or `.dll` files in the ',&
&' # appropriate `lib/` folder, just like it does for static archives. ',&
&' ',&
&'library = false ',&
&' ',&
&'[build] # General Build Options ',&
Expand Down Expand Up @@ -305,6 +309,20 @@ subroutine cmd_new(settings)
&' # This rule applies generally to any number of nested directories and ',&
&' # modules. For example, src/a/b/c/d.f90 must define a module called a_b_c_d. ',&
&' # Again, this is not enforced but may be required in future releases. ',&
&' ',&
&' # Set `shared=true` to build dynamic libraries (.so/.dylib/.dll) ',&
&' # instead of a static archive (.a). When enabled, each package in the ',&
&' # dependency graph will be compiled to its own shared library. ',&
&' # ',&
&' # This is useful for plugin systems, dynamic linking, or when building ',&
&' # language bindings. ',&
&' # ',&
&' # Note: shared libraries are not installed unless `[install] library=true` ',&
&' # is also enabled. ',&
&' # ',&
&' # Example: ',&
&'shared = false ',&

&'']
endif
! create placeholder module src/bname.f90
Expand Down
Loading
Loading