Skip to content
233 changes: 233 additions & 0 deletions src/filesystem_utilities.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
#include <sys/stat.h>
#include <dirent.h>

#ifndef _WIN32
#include <errno.h>
#include <fcntl.h>
#include <spawn.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

extern char **environ;
#endif

#ifdef _WIN32
#include <windows.h>
#include <stdlib.h>
#include <string.h>
#endif

#if defined(__APPLE__) && !defined(__aarch64__) && !defined(__ppc__) && !defined(__i386__)
DIR * opendir$INODE64( const char * dirName );
struct dirent * readdir$INODE64( DIR * dir );
Expand Down Expand Up @@ -29,3 +47,218 @@ struct dirent *c_readdir(DIR *dirp)
{
return readdir(dirp);
}

#ifdef _WIN32
/* Quote one argv token using the CommandLineToArgvW rules so that
CreateProcess reconstructs the exact token. Caller frees the result. */
static char *win_quote_arg(const char *arg)
{
size_t len = strlen(arg);
size_t i;
int needs = (len == 0);
char *out;
char *q;

for (i = 0; i < len; ++i) {
char c = arg[i];
if (c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '"') {
needs = 1;
break;
}
}

if (!needs) {
out = (char *)malloc(len + 1);
if (out) memcpy(out, arg, len + 1);
return out;
}

out = (char *)malloc(2 * len + 3);
if (!out) return NULL;
q = out;
*q++ = '"';
for (i = 0; i < len;) {
size_t nbs = 0;
size_t k;
while (i < len && arg[i] == '\\') { ++nbs; ++i; }
if (i == len) {
for (k = 0; k < 2 * nbs; ++k) *q++ = '\\';
break;
} else if (arg[i] == '"') {
for (k = 0; k < 2 * nbs + 1; ++k) *q++ = '\\';
*q++ = '"';
++i;
} else {
for (k = 0; k < nbs; ++k) *q++ = '\\';
*q++ = arg[i++];
}
}
*q++ = '"';
*q = '\0';
return out;
}
#endif

int c_run_argv(const char *joined, int argc, const char *redirect)
{
#ifdef _WIN32
const char *p = joined;
int i;
int use_redirect = redirect != NULL && redirect[0] != '\0';
const char **argv = NULL;
char *cmd = NULL;
size_t cap = 1, n = 0;
char apppath[MAX_PATH];
char *appname = NULL;
DWORD found;
HANDLE hout = INVALID_HANDLE_VALUE;
SECURITY_ATTRIBUTES sa;
STARTUPINFOA si;
PROCESS_INFORMATION pi;
BOOL ok;
int result;

if (argc < 1) return 0;

argv = (const char **)calloc((size_t)argc + 1, sizeof(char *));
if (argv == NULL) return -1;
for (i = 0; i < argc; ++i) {
argv[i] = p;
p += strlen(p) + 1;
}

/* Reassemble a properly quoted command line for CreateProcess. */
cmd = (char *)malloc(cap);
if (cmd == NULL) { free((void *)argv); return -1; }
cmd[0] = '\0';
for (i = 0; i < argc; ++i) {
char *qa = win_quote_arg(argv[i]);
size_t ql;
size_t need;
if (qa == NULL) { free(cmd); free((void *)argv); return -1; }
ql = strlen(qa);
need = n + ql + 2;
if (need > cap) {
char *t;
cap = need * 2;
t = (char *)realloc(cmd, cap);
if (t == NULL) { free(qa); free(cmd); free((void *)argv); return -1; }
cmd = t;
}
if (i > 0) cmd[n++] = ' ';
memcpy(cmd + n, qa, ql);
n += ql;
cmd[n] = '\0';
free(qa);
}

/* Resolve the program through PATH with a default .exe extension. */
found = SearchPathA(NULL, argv[0], ".exe", MAX_PATH, apppath, NULL);
if (found > 0 && found < MAX_PATH) appname = apppath;

ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));

/* Only override the child's std handles when redirecting. Setting
STARTF_USESTDHANDLES with the (often non-inheritable) console handles
can strip the child's console, so leave them alone otherwise and let
the child inherit the parent console normally. */
if (use_redirect) {
ZeroMemory(&sa, sizeof(sa));
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE;
hout = CreateFileA(redirect, GENERIC_WRITE, FILE_SHARE_READ, &sa,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hout == INVALID_HANDLE_VALUE) { free(cmd); free((void *)argv); return -1; }
si.dwFlags |= STARTF_USESTDHANDLES;
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
si.hStdOutput = hout;
si.hStdError = hout;
}

ok = CreateProcessA(appname, cmd, NULL, NULL, use_redirect, 0, NULL, NULL, &si, &pi);
if (!ok) {
result = -1;
} else {
DWORD code = 1;
WaitForSingleObject(pi.hProcess, INFINITE);
GetExitCodeProcess(pi.hProcess, &code);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
/* Keep a real (possibly high-bit) exit code positive so the caller
does not mistake it for the stat < 0 spawn-failure sentinel. */
result = (code > 0x7fffffff) ? 1 : (int)code;
}

if (hout != INVALID_HANDLE_VALUE) CloseHandle(hout);
free(cmd);
free((void *)argv);
return result;
#else
char **argv = NULL;
const char *p = joined;
pid_t pid;
int spawn_status;
int wait_status;
int i;
int use_redirect = redirect != NULL && redirect[0] != '\0';
posix_spawn_file_actions_t actions;
posix_spawn_file_actions_t *actions_ptr = NULL;

if (argc < 1) {
return 0;
}

argv = calloc((size_t)argc + 1, sizeof(char *));
if (argv == NULL) {
return -1;
}

for (i = 0; i < argc; ++i) {
argv[i] = (char *)p;
p += strlen(p) + 1;
}

if (use_redirect) {
if (posix_spawn_file_actions_init(&actions) != 0) {
free(argv);
return -1;
}
actions_ptr = &actions;
if (posix_spawn_file_actions_addopen(&actions, STDOUT_FILENO, redirect,
O_WRONLY | O_CREAT | O_TRUNC, 0666) != 0 ||
posix_spawn_file_actions_adddup2(&actions, STDOUT_FILENO, STDERR_FILENO) != 0) {
posix_spawn_file_actions_destroy(&actions);
free(argv);
return -1;
}
}

spawn_status = posix_spawnp(&pid, argv[0], actions_ptr, NULL, argv, environ);
if (use_redirect) {
posix_spawn_file_actions_destroy(&actions);
}
free(argv);

if (spawn_status != 0) {
return spawn_status;
}

do {
spawn_status = waitpid(pid, &wait_status, 0);
} while (spawn_status < 0 && errno == EINTR);

if (spawn_status < 0) {
return -1;
}

if (WIFEXITED(wait_status)) {
return WEXITSTATUS(wait_status);
}
if (WIFSIGNALED(wait_status)) {
return 128 + WTERMSIG(wait_status);
}
return -1;
#endif
}
2 changes: 1 addition & 1 deletion src/fpm/manifest/feature.f90
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ subroutine check(table, error)
exit

! Keys
case("description", "platform", "flags", "c-flags", &
case("default", "description", "platform", "flags", "c-flags", &
"cxx-flags", "link-time-flags", "preprocessor", "requires", &
"build", "install", "fortran", "library", "dependencies", &
"dev-dependencies", "executable", "example", "test", "preprocess")
Expand Down
30 changes: 24 additions & 6 deletions src/fpm/manifest/feature_collection.f90
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ module fpm_manifest_feature_collection

! Features shared by specific platform/compiler configurations
type(feature_config_t), allocatable :: variants(:)


! Auto-apply this feature when no explicit --features list is given
logical :: is_default = .false.

contains

procedure :: serializable_is_same => feature_collection_same
Expand Down Expand Up @@ -66,6 +69,7 @@ logical function feature_collection_same(this, that)
feature_collection_same = .false.
select type (other => that)
type is (feature_collection_t)
if (this%is_default .neqv. other%is_default) return
if (.not.(this%base == other%base)) return
if (allocated(this%variants) .neqv. allocated(other%variants)) return
if (allocated(this%variants)) then
Expand All @@ -90,6 +94,9 @@ subroutine feature_collection_dump(self, table, error)
integer :: i
character(len=32) :: key

! default flag
call set_value(table, "default", self%is_default)

! base
call add_table(table, "base", ptr_base)
if (.not. associated(ptr_base)) then
Expand Down Expand Up @@ -124,6 +131,9 @@ subroutine feature_collection_load(self, table, error)
type(toml_key), allocatable :: keys(:)
integer :: i

! default flag
call get_value(table, "default", self%is_default, .false.)

! base (required)
call get_value(table, "base", ptr_base)
if (.not. associated(ptr_base)) then
Expand Down Expand Up @@ -234,16 +244,21 @@ subroutine new_collection_from_subtable(self, table, name, error)
character(*), intent(in) :: name
type(error_t), allocatable, intent(out) :: error

integer :: i
integer :: i, stat
type(platform_config_t) :: default_platform
type(toml_key), allocatable :: keys(:)

logical :: default_val

default_platform = platform_config_t(id_all,OS_ALL)

! Initialize base feature
self%base%name = name
self%base%platform = default_platform


! Parse optional "default" key
call get_value(table, "default", default_val, .false., stat=stat)
if (stat == toml_stat%success) self%is_default = default_val

! Traverse the table hierarchy to find variants
call traverse_feature_table(self, table, name, default_platform, error)
if (allocated(error)) return
Expand Down Expand Up @@ -277,7 +292,10 @@ recursive subroutine traverse_feature_table(collection, table, feature_name, &

! First pass: check what types of keys we have
do i = 1, size(keys)


! Skip collection-level keys handled by the caller
if (keys(i)%key == "default") cycle

! Check if this key is a valid OS name
os_type = match_os_type(keys(i)%key)
if (os_type /= OS_UNKNOWN) then
Expand Down
10 changes: 10 additions & 0 deletions src/fpm/manifest/package.f90
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,16 @@ type(package_config_t) function export_config(self, platform, features, profile,
call apply_default_features(self, self%profiles(idx), cfg, platform, verbose, error)
if (allocated(error)) return
end if

! Apply features marked default = true in the manifest.
if (allocated(self%features)) then
do i = 1, size(self%features)
if (self%features(i)%is_default) then
call self%features(i)%merge_into_package(cfg, platform, error)
if (allocated(error)) return
end if
end do
end if
end if

! Then apply the requested features on top
Expand Down
7 changes: 5 additions & 2 deletions src/fpm_backend.F90
Original file line number Diff line number Diff line change
Expand Up @@ -327,11 +327,14 @@ subroutine build_target(model,target,verbose,dry_run,table,stat)

integer :: fh

!$omp critical
! mkdir shells out (is_dir 'test -d' and 'mkdir -p'), which forks. Serialize
! it on the same run_command lock as every other shell fork so it cannot run
! concurrently with a link/archive/compile shell fallback on another thread.
!$omp critical (run_command)
if (.not.exists(dirname(target%output_file)) .and. .not.dry_run) then
call mkdir(dirname(target%output_file),verbose)
end if
!$omp end critical
!$omp end critical (run_command)

select case(target%target_type)

Expand Down
13 changes: 10 additions & 3 deletions src/fpm_command_line.f90
Original file line number Diff line number Diff line change
Expand Up @@ -1663,9 +1663,16 @@ subroutine build_settings(self, list, show_model, build_tests, config_file)
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)
! Parse comma-separated features and profiles.
! --features "openmp,mpi" → apply listed features (no auto-defaults).
! --features "" → explicit empty list (no auto-defaults).
! (not specified) → auto-default features apply.
if (specified('features')) then
if (len_trim(feats) > 0) then
call parse_features(feats, self%features)
else
allocate(self%features(0))
end if
else
if (allocated(self%features)) deallocate(self%features)
end if
Expand Down
Loading
Loading