Skip to content

Commit 76bbfc5

Browse files
authored
Merge pull request #1589 from oesteban/enh/issue-1571
ENH: Do not allow writing derivatives directly into the BIDS root folder
2 parents a3e185b + bea5939 commit 76bbfc5

File tree

1 file changed

+67
-59
lines changed

1 file changed

+67
-59
lines changed

fmriprep/cli/run.py

Lines changed: 67 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
"""
88

99
import os
10-
import os.path as op
1110
from pathlib import Path
1211
import logging
1312
import sys
@@ -56,10 +55,10 @@ def get_parser():
5655
# Arguments as specified by BIDS-Apps
5756
# required, positional arguments
5857
# IMPORTANT: they must go directly with the parser object
59-
parser.add_argument('bids_dir', action='store',
58+
parser.add_argument('bids_dir', action='store', type=Path,
6059
help='the root folder of a BIDS valid dataset (sub-XXXXX folders should '
6160
'be found at the top level in this folder).')
62-
parser.add_argument('output_dir', action='store',
61+
parser.add_argument('output_dir', action='store', type=Path,
6362
help='the output path for the outcomes of preprocessing and visual '
6463
'reports')
6564
parser.add_argument('analysis_level', choices=['participant'],
@@ -228,7 +227,7 @@ def get_parser():
228227
' Use `--fs-no-reconall` instead.')
229228

230229
g_other = parser.add_argument_group('Other options')
231-
g_other.add_argument('-w', '--work-dir', action='store',
230+
g_other.add_argument('-w', '--work-dir', action='store', type=Path, default=Path('work'),
232231
help='path where intermediate results should be stored')
233232
g_other.add_argument(
234233
'--resource-monitor', action='store_true', default=False,
@@ -323,10 +322,10 @@ def before_send(event, hints):
323322
if exec_env == 'fmriprep-docker':
324323
scope.set_tag('docker_version', os.getenv('DOCKER_VERSION_8395080871'))
325324

326-
dset_desc_path = os.path.join(opts.bids_dir, 'dataset_description.json')
327-
if os.path.exists(dset_desc_path):
328-
with open(dset_desc_path, 'rb') as fp:
329-
scope.set_tag('dset_desc_sha256', hashlib.sha256(fp.read()).hexdigest())
325+
dset_desc_path = opts.bids_dir / 'dataset_description.json'
326+
if dset_desc_path.exists():
327+
desc_content = dset_desc_path.read_bytes()
328+
scope.set_tag('dset_desc_sha256', hashlib.sha256(desc_content).hexdigest())
330329

331330
free_mem_at_start = round(psutil.virtual_memory().free / 1024**3, 1)
332331
scope.set_tag('free_mem_at_start', free_mem_at_start)
@@ -361,7 +360,7 @@ def before_send(event, hints):
361360
if not opts.skip_bids_validation:
362361
print("Making sure the input data is BIDS compliant (warnings can be ignored in most "
363362
"cases).")
364-
validate_input_dir(exec_env, opts.bids_dir, opts.participant_label)
363+
validate_input_dir(exec_env, str(opts.bids_dir), opts.participant_label)
365364

366365
# FreeSurfer license
367366
default_license = str(Path(os.getenv('FREESURFER_HOME')) / 'license.txt')
@@ -394,39 +393,28 @@ def before_send(event, hints):
394393
p.start()
395394
p.join()
396395

397-
if p.exitcode != 0:
398-
sys.exit(p.exitcode)
399-
400-
fmriprep_wf = retval['workflow']
401-
plugin_settings = retval['plugin_settings']
402-
bids_dir = retval['bids_dir']
403-
output_dir = retval['output_dir']
404-
work_dir = retval['work_dir']
405-
subject_list = retval['subject_list']
406-
run_uuid = retval['run_uuid']
407-
if not opts.notrack:
408-
with sentry_sdk.configure_scope() as scope:
409-
scope.set_tag('run_uuid', run_uuid)
410-
scope.set_tag('npart', len(subject_list))
411-
412-
retcode = retval['return_code']
396+
retcode = p.exitcode or retval.get('return_code', 0)
413397

414-
if fmriprep_wf is None:
415-
sys.exit(1)
416-
417-
if opts.write_graph:
418-
fmriprep_wf.write_graph(graph2use="colored", format='svg', simple_form=True)
398+
bids_dir = retval.get('bids_dir')
399+
output_dir = retval.get('output_dir')
400+
work_dir = retval.get('work_dir')
401+
plugin_settings = retval.get('plugin_settings', None)
402+
subject_list = retval.get('subject_list', None)
403+
fmriprep_wf = retval.get('workflow', None)
404+
run_uuid = retval.get('run_uuid', None)
419405

420406
if opts.reports_only:
421407
sys.exit(int(retcode > 0))
422408

423409
if opts.boilerplate:
424410
sys.exit(int(retcode > 0))
425411

426-
# Sentry tracking
427-
if not opts.notrack:
428-
sentry_sdk.add_breadcrumb(message='fMRIPrep started', level='info')
429-
sentry_sdk.capture_message('fMRIPrep started', level='info')
412+
if fmriprep_wf and opts.write_graph:
413+
fmriprep_wf.write_graph(graph2use="colored", format='svg', simple_form=True)
414+
415+
retcode = retcode or int(fmriprep_wf is None)
416+
if retcode != 0:
417+
sys.exit(retcode)
430418

431419
# Check workflow for missing commands
432420
missing = check_deps(fmriprep_wf)
@@ -435,9 +423,19 @@ def before_send(event, hints):
435423
for iface, cmd in missing:
436424
print("\t{} (Interface: {})".format(cmd, iface))
437425
sys.exit(2)
438-
439426
# Clean up master process before running workflow, which may create forks
440427
gc.collect()
428+
429+
# Sentry tracking
430+
if not opts.notrack:
431+
with sentry_sdk.configure_scope() as scope:
432+
if run_uuid:
433+
scope.set_tag('run_uuid', run_uuid)
434+
if subject_list:
435+
scope.set_tag('npart', len(subject_list))
436+
sentry_sdk.add_breadcrumb(message='fMRIPrep started', level='info')
437+
sentry_sdk.capture_message('fMRIPrep started', level='info')
438+
441439
try:
442440
fmriprep_wf.run(**plugin_settings)
443441
except RuntimeError as e:
@@ -589,6 +587,24 @@ def build_workflow(opts, retval):
589587
* Run identifier: {uuid}.
590588
""".format
591589

590+
bids_dir = opts.bids_dir.resolve()
591+
output_dir = opts.output_dir.resolve()
592+
work_dir = opts.work_dir
593+
594+
retval['return_code'] = 1
595+
retval['workflow'] = None
596+
retval['bids_dir'] = str(bids_dir)
597+
retval['output_dir'] = str(output_dir)
598+
retval['work_dir'] = str(work_dir)
599+
600+
if output_dir == bids_dir:
601+
logger.error(
602+
'The selected output folder is the same as the input BIDS folder. '
603+
'Please modify the output path (suggestion: %s).',
604+
bids_dir / 'derivatives' / ('fmriprep-%s' % __version__.split('+')[0]))
605+
retval['return_code'] = 1
606+
return retval
607+
592608
# Reduce to unique space identifiers
593609
output_spaces = sorted(set(opts.output_space))
594610

@@ -634,12 +650,13 @@ def build_workflow(opts, retval):
634650

635651
# Set up some instrumental utilities
636652
run_uuid = '%s_%s' % (strftime('%Y%m%d-%H%M%S'), uuid.uuid4())
653+
retval['run_uuid'] = run_uuid
637654

638655
# First check that bids_dir looks like a BIDS folder
639-
bids_dir = os.path.abspath(opts.bids_dir)
640-
layout = BIDSLayout(bids_dir, validate=False)
656+
layout = BIDSLayout(str(bids_dir), validate=False)
641657
subject_list = collect_participants(
642658
layout, participant_label=opts.participant_label)
659+
retval['subject_list'] = subject_list
643660

644661
# Load base plugin_settings from file if --use-plugin
645662
if opts.use_plugin is not None:
@@ -679,28 +696,26 @@ def build_workflow(opts, retval):
679696
logger.warning(
680697
'Per-process threads (--omp-nthreads=%d) exceed total '
681698
'threads (--nthreads/--n_cpus=%d)', omp_nthreads, nthreads)
699+
retval['plugin_settings'] = plugin_settings
682700

683701
# Set up directories
684-
output_dir = op.abspath(opts.output_dir)
685-
log_dir = op.join(output_dir, 'fmriprep', 'logs')
686-
work_dir = op.abspath(opts.work_dir or 'work') # Set work/ as default
687-
702+
log_dir = output_dir / 'fmriprep' / 'logs'
688703
# Check and create output and working directories
689-
os.makedirs(output_dir, exist_ok=True)
690-
os.makedirs(log_dir, exist_ok=True)
691-
os.makedirs(work_dir, exist_ok=True)
704+
output_dir.mkdir(exist_ok=True, parents=True)
705+
log_dir.mkdir(exist_ok=True, parents=True)
706+
work_dir.mkdir(exist_ok=True, parents=True)
692707

693708
# Nipype config (logs and execution)
694709
ncfg.update_config({
695710
'logging': {
696-
'log_directory': log_dir,
711+
'log_directory': str(log_dir),
697712
'log_to_file': True
698713
},
699714
'execution': {
700-
'crashdump_dir': log_dir,
715+
'crashdump_dir': str(log_dir),
701716
'crashfile_format': 'txt',
702717
'get_linked_libs': False,
703-
'stop_on_first_crash': opts.stop_on_first_crash or opts.work_dir is None,
718+
'stop_on_first_crash': opts.stop_on_first_crash,
704719
},
705720
'monitoring': {
706721
'enabled': opts.resource_monitor,
@@ -712,21 +727,14 @@ def build_workflow(opts, retval):
712727
if opts.resource_monitor:
713728
ncfg.enable_resource_monitor()
714729

715-
retval['return_code'] = 0
716-
retval['plugin_settings'] = plugin_settings
717-
retval['bids_dir'] = bids_dir
718-
retval['output_dir'] = output_dir
719-
retval['work_dir'] = work_dir
720-
retval['subject_list'] = subject_list
721-
retval['run_uuid'] = run_uuid
722-
retval['workflow'] = None
723-
724730
# Called with reports only
725731
if opts.reports_only:
726732
logger.log(25, 'Running --reports-only on participants %s', ', '.join(subject_list))
727733
if opts.run_uuid is not None:
728734
run_uuid = opts.run_uuid
729-
retval['return_code'] = generate_reports(subject_list, output_dir, work_dir, run_uuid)
735+
retval['run_uuid'] = run_uuid
736+
retval['return_code'] = generate_reports(
737+
subject_list, str(output_dir), str(work_dir), run_uuid)
730738
return retval
731739

732740
# Build main workflow
@@ -761,8 +769,8 @@ def build_workflow(opts, retval):
761769
omp_nthreads=omp_nthreads,
762770
skull_strip_template=opts.skull_strip_template,
763771
skull_strip_fixed_seed=opts.skull_strip_fixed_seed,
764-
work_dir=work_dir,
765-
output_dir=output_dir,
772+
work_dir=str(work_dir),
773+
output_dir=str(output_dir),
766774
freesurfer=opts.run_reconall,
767775
output_spaces=output_spaces,
768776
template=opts.template,

0 commit comments

Comments
 (0)