Skip to content

Commit cd779a9

Browse files
author
Peter Amstutz
authored
Symlink err (#1138)
* Check for file staging conflicts * Directories can be merged, don't error. * Dereference symlinks to avoid operating on the same file with different aliases * Test that output files with same name get renamed
1 parent 6300a49 commit cd779a9

File tree

5 files changed

+90
-8
lines changed

5 files changed

+90
-8
lines changed

cwltool/argparser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ def arg_parser(): # type: () -> argparse.ArgumentParser
252252

253253
exgroup = parser.add_mutually_exclusive_group()
254254
exgroup.add_argument("--enable-color", action="store_true",
255-
help="Enable colored logging (default true)", default=True)
255+
help="Enable logging color (default enabled)", default=True)
256256
exgroup.add_argument("--disable-color", action="store_false", dest="enable_color",
257257
help="Disable colored logging (default false)")
258258

cwltool/pathmapper.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -340,8 +340,10 @@ def reversemap(self,
340340
return None
341341

342342
def update(self, key, resolved, target, ctype, stage):
343-
# type: (Text, Text, Text, Text, bool) -> None
344-
self._pathmap[key] = MapperEnt(resolved, target, ctype, stage)
343+
# type: (Text, Text, Text, Text, bool) -> MapperEnt
344+
m = MapperEnt(resolved, target, ctype, stage)
345+
self._pathmap[key] = m
346+
return m
345347

346348
def __contains__(self, key): # type: (Text) -> bool
347349
"""Test for the presence of the given relative path in this mapper."""

cwltool/process.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from typing_extensions import (TYPE_CHECKING, # pylint: disable=unused-import
2929
Text)
3030
from schema_salad import schema, validate
31-
from schema_salad.ref_resolver import Loader, file_uri
31+
from schema_salad.ref_resolver import Loader, file_uri, uri_file_path
3232
from schema_salad.sourceline import SourceLine, strip_dup_lineno
3333

3434
from . import expression
@@ -39,7 +39,8 @@
3939
from .loghandler import _logger
4040
from .mutation import MutationManager # pylint: disable=unused-import
4141
from .pathmapper import (PathMapper, adjustDirObjs, ensure_writable,
42-
get_listing, normalizeFilesDirs, visit_class)
42+
get_listing, normalizeFilesDirs, visit_class,
43+
MapperEnt)
4344
from .secrets import SecretStore # pylint: disable=unused-import
4445
from .software_requirements import ( # pylint: disable=unused-import
4546
DependenciesConfiguration)
@@ -222,9 +223,30 @@ def stage_files(pathmapper, # type: PathMapper
222223
stage_func=None, # type: Optional[Callable[..., Any]]
223224
ignore_writable=False, # type: bool
224225
symlink=True, # type: bool
225-
secret_store=None # type: Optional[SecretStore]
226+
secret_store=None, # type: Optional[SecretStore]
227+
fix_conflicts=False # type: bool
226228
): # type: (...) -> None
227229
"""Link or copy files to their targets. Create them as needed."""
230+
231+
targets = {} # type: Dict[Text, MapperEnt]
232+
for key, entry in pathmapper.items():
233+
if not 'File' in entry.type:
234+
continue
235+
if entry.target not in targets:
236+
targets[entry.target] = entry
237+
elif targets[entry.target].resolved != entry.resolved:
238+
if fix_conflicts:
239+
tgt = entry.target
240+
i = 2
241+
tgt = "%s_%s" % (tgt, i)
242+
while tgt in targets:
243+
i += 1
244+
tgt = "%s_%s" % (tgt, i)
245+
targets[tgt] = pathmapper.update(key, entry.resolved, tgt, entry.type, entry.staged)
246+
else:
247+
raise WorkflowException("File staging conflict, trying to stage both %s and %s to the same target %s" % (
248+
targets[entry.target].resolved, entry.resolved, entry.target))
249+
228250
for key, entry in pathmapper.items():
229251
if not entry.staged:
230252
continue
@@ -329,9 +351,16 @@ def _relocate(src, dst): # type: (Text, Text) -> None
329351
else:
330352
shutil.copy2(src, dst)
331353

354+
def _realpath(ob): # type: (Dict[Text, Any]) -> None
355+
if ob["location"].startswith("file:"):
356+
ob["location"] = file_uri(os.path.realpath(uri_file_path(ob["location"])))
357+
if ob["location"].startswith("/"):
358+
ob["location"] = os.path.realpath(ob["location"])
359+
332360
outfiles = list(_collectDirEntries(outputObj))
361+
visit_class(outfiles, ("File", "Directory"), _realpath)
333362
pm = path_mapper(outfiles, "", destination_path, separateDirs=False)
334-
stage_files(pm, stage_func=_relocate, symlink=False)
363+
stage_files(pm, stage_func=_relocate, symlink=False, fix_conflicts=True)
335364

336365
def _check_adjust(a_file): # type: (Dict[Text, Text]) -> Dict[Text, Text]
337366
a_file["location"] = file_uri(pm.mapper(a_file["location"])[1])

tests/test_relocate.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,27 @@
1+
import sys
2+
import json
3+
4+
if sys.version_info[0] < 3:
5+
from StringIO import StringIO
6+
else:
7+
from io import StringIO
8+
19
from cwltool.main import main
210

3-
from .util import get_data, needs_docker
11+
from .util import get_data, needs_docker, temp_dir
412

513
@needs_docker
614
def test_for_910():
715
assert main([get_data('tests/wf/910.cwl')]) == 0
816
assert main([get_data('tests/wf/910.cwl')]) == 0
17+
18+
@needs_docker
19+
def test_for_conflict_file_names():
20+
stream = StringIO()
21+
22+
with temp_dir() as tmp:
23+
assert main(["--debug", "--outdir", tmp, get_data('tests/wf/conflict.cwl')], stdout=stream) == 0
24+
25+
out = json.loads(stream.getvalue())
26+
assert out["b1"]["basename"] == out["b2"]["basename"]
27+
assert out["b1"]["location"] != out["b2"]["location"]

tests/wf/conflict.cwl

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
cwlVersion: v1.1
2+
$graph:
3+
- class: CommandLineTool
4+
id: makebzz
5+
inputs: []
6+
outputs:
7+
bzz:
8+
type: File
9+
outputBinding:
10+
glob: bzz
11+
requirements:
12+
ShellCommandRequirement: {}
13+
arguments: [{shellQuote: false, valueFrom: "touch bzz"}]
14+
- class: Workflow
15+
id: main
16+
inputs: []
17+
outputs:
18+
b1:
19+
type: File
20+
outputSource: step1/bzz
21+
b2:
22+
type: File
23+
outputSource: step2/bzz
24+
steps:
25+
step1:
26+
in: {}
27+
out: [bzz]
28+
run: '#makebzz'
29+
step2:
30+
in: {}
31+
out: [bzz]
32+
run: '#makebzz'

0 commit comments

Comments
 (0)