Repository: common-workflow-language/cwltool

Issue:
error if symlinks with absolute paths are present inside Directory outputs
```
#!/usr/bin/env cwl-runner
cwlVersion: v1.0
class: CommandLineTool
inputs: []
baseCommand: [ bash, -c ]
arguments:
 - "mkdir foo; echo 42 > foo/bar; ln -s  $PWD/foo/bar foo/baz"
outputs:
  result:
    type: Directory
    outputBinding:
      glob: foo
```

Short term workaround: use `--copy-outputs` but that leaves intermediate files laying around afterwards.

Example error:

```
$ TMPDIR=$PWD cwltool   --debug tests/symlinks.cwl
INFO /home/michael/ebi/env/bin/cwltool 3.1.20210623153106
INFO Resolved 'tests/symlinks.cwl' to 'file:///home/michael/cwltool/tests/symlinks.cwl'
DEBUG Parsed job order from command line: {
    "id": "tests/symlinks.cwl"
}
DEBUG [job symlinks.cwl] initializing from file:///home/michael/cwltool/tests/symlinks.cwl
DEBUG [job symlinks.cwl] {}
DEBUG [job symlinks.cwl] path mappings is {}
DEBUG [job symlinks.cwl] command line bindings is [
    {
        "position": [
            -1000000,
            0
        ],
        "datum": "bash"
    },
    {
        "position": [
            -1000000,
            1
        ],
        "datum": "-c"
    },
    {
        "position": [
            0,
            0
        ],
        "datum": "mkdir foo; echo 42 > foo/bar; ln -s $PWD/foo/bar foo/baz"
    }
]
DEBUG [job symlinks.cwl] initial work dir {}
INFO [job symlinks.cwl] /home/michael/cwltool/l6s28ubx$ bash \
    -c \
    'mkdir foo; echo 42 > foo/bar; ln -s $PWD/foo/bar foo/baz'
DEBUG Could not collect memory usage, job ended before monitoring began.
INFO [job symlinks.cwl] completed success
DEBUG [job symlinks.cwl] outputs {
    "result": {
        "location": "file:///home/michael/cwltool/l6s28ubx/foo",
        "basename": "foo",
        "nameroot": "foo",
        "nameext": "",
        "class": "Directory"
    }
}
DEBUG [job symlinks.cwl] Removing input staging directory /home/michael/cwltool/tp94m_vi
DEBUG [job symlinks.cwl] Removing temporary directory /home/michael/cwltool/2zomy12a
DEBUG Moving /home/michael/cwltool/l6s28ubx/foo to /home/michael/cwltool/foo
DEBUG Moving /home/michael/cwltool/l6s28ubx/foo/bar to /home/michael/cwltool/foo/bar
DEBUG Moving /home/michael/cwltool/l6s28ubx/foo/bar to /home/michael/cwltool/foo/baz
ERROR Unhandled error:
  [Errno 2] No such file or directory: '/home/michael/cwltool/l6s28ubx/foo/bar'
Traceback (most recent call last):
  File "/usr/lib/python3.9/shutil.py", line 806, in move
    os.rename(src, real_dst)
FileNotFoundError: [Errno 2] No such file or directory: '/home/michael/cwltool/l6s28ubx/foo/bar' -> '/home/michael/cwltool/foo/baz'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/michael/cwltool/cwltool/main.py", line 1248, in main
    (out, status) = real_executor(
  File "/home/michael/cwltool/cwltool/executors.py", line 59, in __call__
    return self.execute(process, job_order_object, runtime_context, logger)
  File "/home/michael/cwltool/cwltool/executors.py", line 155, in execute
    self.final_output[0] = relocateOutputs(
  File "/home/michael/cwltool/cwltool/process.py", line 400, in relocateOutputs
    stage_files(pm, stage_func=_relocate, symlink=False, fix_conflicts=True)
  File "/home/michael/cwltool/cwltool/process.py", line 296, in stage_files
    stage_func(entry.resolved, entry.target)
  File "/home/michael/cwltool/cwltool/process.py", line 371, in _relocate
    _relocate(dir_entry.path, fs_access.join(dst, dir_entry.name))
  File "/home/michael/cwltool/cwltool/process.py", line 373, in _relocate
    shutil.move(src, dst)
  File "/usr/lib/python3.9/shutil.py", line 820, in move
    copy_function(src, real_dst)
  File "/usr/lib/python3.9/shutil.py", line 435, in copy2
    copyfile(src, dst, follow_symlinks=follow_symlinks)
  File "/usr/lib/python3.9/shutil.py", line 264, in copyfile
    with open(src, 'rb') as fsrc, open(dst, 'wb') as fdst:
FileNotFoundError: [Errno 2] No such file or directory: '/home/michael/cwltool/l6s28ubx/foo/bar'
```


Discussion:

Resolving patch:
diff --git a/cwltool/process.py b/cwltool/process.py
--- a/cwltool/process.py
+++ b/cwltool/process.py
@@ -356,19 +356,28 @@ def _relocate(src: str, dst: str) -> None:
             return
 
         # If the source is not contained in source_directories we're not allowed to delete it
-        src = fs_access.realpath(src)
+        real_src = fs_access.realpath(src)
         src_can_deleted = any(
-            os.path.commonprefix([p, src]) == p for p in source_directories
+            os.path.commonprefix([p, real_src]) == p for p in source_directories
         )
 
         _action = "move" if action == "move" and src_can_deleted else "copy"
 
         if _action == "move":
             _logger.debug("Moving %s to %s", src, dst)
-            if fs_access.isdir(src) and fs_access.isdir(dst):
-                # merge directories
-                for dir_entry in scandir(src):
-                    _relocate(dir_entry.path, fs_access.join(dst, dir_entry.name))
+            if fs_access.isdir(src):
+                if fs_access.isdir(dst):
+                    if len(fs_access.listdir(dst)) > 0:
+                        # merge directories
+                        for dir_entry in scandir(src):
+                            _relocate(
+                                dir_entry.path, fs_access.join(dst, dir_entry.name)
+                            )
+                    else:
+                        os.rmdir(dst)
+                        shutil.move(src, dst)
+                else:
+                    shutil.move(src, dst)
             else:
                 shutil.move(src, dst)
 


Test patch:
diff --git a/tests/symlinks.cwl b/tests/symlinks.cwl
new file mode 100644
--- /dev/null
+++ b/tests/symlinks.cwl
@@ -0,0 +1,14 @@
+#!/usr/bin/env cwl-runner
+cwlVersion: v1.0
+class: CommandLineTool
+inputs: []
+baseCommand: [ bash, -c ]
+arguments:
+# - "mkdir foo; echo 42 > foo/bar; ln -s bar foo/baz"
+ - "mkdir foo; echo 42 > foo/bar; ln -s $PWD/foo/bar foo/baz"
+# - "mkdir foo; ln -s $PWD/foo/bar foo/baz ; echo 42 > foo/bar"
+outputs:
+  result:
+    type: Directory
+    outputBinding:
+      glob: foo
diff --git a/tests/test_relocate.py b/tests/test_relocate.py
--- a/tests/test_relocate.py
+++ b/tests/test_relocate.py
@@ -6,10 +6,7 @@
 
 from .util import get_data, needs_docker
 
-if sys.version_info[0] < 3:
-    from StringIO import StringIO
-else:
-    from io import StringIO
+from io import StringIO
 
 
 @needs_docker
@@ -18,6 +15,21 @@ def test_for_910() -> None:
     assert main([get_data("tests/wf/910.cwl")]) == 0
 
 
+def test_symlinks_with_absolute_paths(tmp_path: Path) -> None:
+    """Confirm that absolute paths in Directory types don't cause problems."""
+    assert (
+        main(
+            [
+                "--debug",
+                f"--outdir={tmp_path}/result",
+                f"--tmpdir-prefix={tmp_path}/tmp",
+                get_data("tests/symlinks.cwl"),
+            ]
+        )
+        == 0
+    )
+
+
 @needs_docker
 def test_for_conflict_file_names(tmp_path: Path) -> None:
     stream = StringIO()