Skip to content

Commit 000b07c

Browse files
authored
Merge pull request #419 from kapilkd13/testing-windows
Windows Compatibility: ensuring Cwltool works on windows OS
2 parents 9a7047b + 06dd0b2 commit 000b07c

20 files changed

+403
-75
lines changed

appveyor.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
version: .{build}-{branch}
2+
3+
environment:
4+
5+
matrix:
6+
- PYTHON: "C:\\Python27"
7+
PYTHON_VERSION: "2.7.x"
8+
PYTHON_ARCH: "32"
9+
10+
- PYTHON: "C:\\Python27-x64"
11+
PYTHON_VERSION: "2.7.x"
12+
PYTHON_ARCH: "64"
13+
14+
install:
15+
- "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
16+
- "python --version"
17+
- "python -c \"import struct; print(struct.calcsize('P') * 8)\""
18+
- "pip install --disable-pip-version-check --user --upgrade pip"
19+
- "pip install --upgrade --no-deps --force-reinstall git+git://github.com/kapilkd13/schema_salad@windows#egg=schema_salad-2.4.201706261942"
20+
21+
build_script:
22+
- "%CMD_IN_ENV% python setup.py install"
23+
24+
25+
test_script:
26+
27+
- "%CMD_IN_ENV% python setup.py test"
28+

cwltool/builder.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import absolute_import
22
import copy
3+
import os
34
from typing import Any, Callable, Dict, List, Text, Type, Union
45

56
import six
@@ -15,7 +16,7 @@
1516
from .pathmapper import (PathMapper, get_listing, normalizeFilesDirs,
1617
visit_class)
1718
from .stdfsaccess import StdFsAccess
18-
from .utils import aslist
19+
from .utils import aslist, get_feature, docker_windows_path_adjust, onWindows
1920

2021
# if six.PY3:
2122
# AvroSchemaFromJSONData = avro.schema.SchemaFromJSONData
@@ -185,6 +186,11 @@ def tostr(self, value): # type: (Any) -> Text
185186
if isinstance(value, dict) and value.get("class") in ("File", "Directory"):
186187
if "path" not in value:
187188
raise WorkflowException(u"%s object missing \"path\": %s" % (value["class"], value))
189+
190+
# Path adjust for windows file path when passing to docker, docker accepts unix like path only
191+
(docker_req, docker_is_req) = get_feature(self, "DockerRequirement")
192+
if onWindows() and docker_req is not None: # docker_req is none only when there is no dockerRequirement mentioned in hints and Requirement
193+
return docker_windows_path_adjust(value["path"])
188194
return value["path"]
189195
else:
190196
return Text(value)

cwltool/draft2tool.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
_logger_validation_warnings, compute_checksums,
2929
normalizeFilesDirs, shortname, uniquename)
3030
from .stdfsaccess import StdFsAccess
31-
from .utils import aslist
31+
from .utils import aslist, docker_windows_path_adjust, convert_pathsep_to_unix
3232
from six.moves import map
3333

3434
ACCEPTLIST_EN_STRICT_RE = re.compile(r"^[a-zA-Z0-9._+-]+$")
@@ -107,7 +107,7 @@ def revmap_file(builder, outdir, f):
107107

108108
if "location" in f:
109109
if f["location"].startswith("file://"):
110-
path = uri_file_path(f["location"])
110+
path = convert_pathsep_to_unix(uri_file_path(f["location"]))
111111
revmap_f = builder.pathmapper.reversemap(path)
112112
if revmap_f:
113113
f["location"] = revmap_f[1]
@@ -156,7 +156,7 @@ def run(self, **kwargs):
156156
def check_adjust(builder, f):
157157
# type: (Builder, Dict[Text, Any]) -> Dict[Text, Any]
158158

159-
f["path"] = builder.pathmapper.mapper(f["location"])[1]
159+
f["path"] = docker_windows_path_adjust(builder.pathmapper.mapper(f["location"])[1])
160160
f["dirname"], f["basename"] = os.path.split(f["path"])
161161
if f["class"] == "File":
162162
f["nameroot"], f["nameext"] = os.path.splitext(f["basename"])
@@ -230,6 +230,10 @@ def job(self,
230230
(docker_req, docker_is_req) = self.get_requirement("DockerRequirement")
231231
if docker_req and kwargs.get("use_container") is not False:
232232
dockerimg = docker_req.get("dockerImageId") or docker_req.get("dockerPull")
233+
elif kwargs.get("default_container", None) is not None and kwargs.get("use_container") is not False:
234+
dockerimg = kwargs.get("default_container")
235+
236+
if dockerimg:
233237
cmdline = ["docker", "run", dockerimg] + cmdline
234238
keydict = {u"cmdline": cmdline}
235239

cwltool/expression.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import logging
55
import re
66
from typing import Any, AnyStr, Dict, List, Text, Union
7+
from .utils import docker_windows_path_adjust
78
import six
89
from six import u
910

@@ -203,8 +204,8 @@ def do_eval(ex, jobinput, requirements, outdir, tmpdir, resources,
203204
# type: (Union[dict, AnyStr], Dict[Text, Union[Dict, List, Text]], List[Dict[Text, Any]], Text, Text, Dict[Text, Union[int, Text]], Any, bool, int, bool) -> Any
204205

205206
runtime = copy.copy(resources)
206-
runtime["tmpdir"] = tmpdir
207-
runtime["outdir"] = outdir
207+
runtime["tmpdir"] = docker_windows_path_adjust(tmpdir)
208+
runtime["outdir"] = docker_windows_path_adjust(outdir)
208209

209210
rootvars = {
210211
u"inputs": jobinput,

cwltool/job.py

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import shellescape
1818

19+
from .utils import copytree_with_merge, docker_windows_path_adjust, onWindows
1920
from . import docker
2021
from .builder import Builder
2122
from .docker_uid import docker_vm_uid
@@ -106,8 +107,14 @@ def relink_initialworkdir(pathmapper, inplace_update=False):
106107
if os.path.islink(vol.target) or os.path.isfile(vol.target):
107108
os.remove(vol.target)
108109
elif os.path.isdir(vol.target):
109-
os.rmdir(vol.target)
110-
os.symlink(vol.resolved, vol.target)
110+
shutil.rmtree(vol.target)
111+
if onWindows():
112+
if vol.type in ("File", "WritableFile"):
113+
shutil.copy(vol.resolved,vol.target)
114+
elif vol.type in ("Directory", "WritableDirectory"):
115+
copytree_with_merge(vol.resolved, vol.target)
116+
else:
117+
os.symlink(vol.resolved, vol.target)
111118

112119
class JobBase(object):
113120
def __init__(self): # type: () -> None
@@ -278,13 +285,16 @@ def run(self, pull_image=True, rm_container=True,
278285
if vars_to_preserve is not None:
279286
for key, value in os.environ.items():
280287
if key in vars_to_preserve and key not in env:
281-
env[key] = value
282-
env["HOME"] = self.outdir
283-
env["TMPDIR"] = self.tmpdir
284-
285-
stageFiles(self.pathmapper, os.symlink, ignoreWritable=True)
288+
# On Windows, subprocess env can't handle unicode.
289+
env[key] = str(value) if onWindows() else value
290+
env["HOME"] = str(self.outdir) if onWindows() else self.outdir
291+
env["TMPDIR"] = str(self.tmpdir) if onWindows() else self.tmpdir
292+
if "PATH" not in env:
293+
env["PATH"] = str(os.environ["PATH"]) if onWindows() else os.environ["PATH"]
294+
295+
stageFiles(self.pathmapper, ignoreWritable=True, symLink=True)
286296
if self.generatemapper:
287-
stageFiles(self.generatemapper, os.symlink, ignoreWritable=self.inplace_update)
297+
stageFiles(self.generatemapper, ignoreWritable=self.inplace_update, symLink=True)
288298
relink_initialworkdir(self.generatemapper, inplace_update=self.inplace_update)
289299

290300
self._execute([], env, rm_tmpdir=rm_tmpdir, move_outputs=move_outputs)
@@ -306,25 +316,26 @@ def add_volumes(self, pathmapper, runtime, stage_output):
306316
containertgt = vol.target
307317
if vol.type in ("File", "Directory"):
308318
if not vol.resolved.startswith("_:"):
309-
runtime.append(u"--volume=%s:%s:ro" % (vol.resolved, containertgt))
319+
runtime.append(u"--volume=%s:%s:ro" % (docker_windows_path_adjust(vol.resolved), docker_windows_path_adjust(containertgt)))
310320
elif vol.type == "WritableFile":
311321
if self.inplace_update:
312-
runtime.append(u"--volume=%s:%s:rw" % (vol.resolved, containertgt))
322+
runtime.append(u"--volume=%s:%s:rw" % (docker_windows_path_adjust(vol.resolved), docker_windows_path_adjust(containertgt)))
313323
else:
314324
shutil.copy(vol.resolved, vol.target)
315325
elif vol.type == "WritableDirectory":
316326
if vol.resolved.startswith("_:"):
317327
os.makedirs(vol.target, 0o0755)
318328
else:
319329
if self.inplace_update:
320-
runtime.append(u"--volume=%s:%s:rw" % (vol.resolved, containertgt))
330+
runtime.append(u"--volume=%s:%s:rw" % (docker_windows_path_adjust(vol.resolved), docker_windows_path_adjust(containertgt)))
321331
else:
322332
shutil.copytree(vol.resolved, vol.target)
323333
elif vol.type == "CreateFile":
324334
createtmp = os.path.join(host_outdir, os.path.basename(vol.target))
325335
with open(createtmp, "wb") as f:
326336
f.write(vol.resolved.encode("utf-8"))
327-
runtime.append(u"--volume=%s:%s:ro" % (createtmp, vol.target))
337+
runtime.append(u"--volume=%s:%s:ro" % (docker_windows_path_adjust(createtmp), docker_windows_path_adjust(vol.target)))
338+
328339

329340
def run(self, pull_image=True, rm_container=True,
330341
rm_tmpdir=True, move_outputs="move", **kwargs):
@@ -361,14 +372,14 @@ def run(self, pull_image=True, rm_container=True,
361372

362373
runtime = [u"docker", u"run", u"-i"]
363374

364-
runtime.append(u"--volume=%s:%s:rw" % (os.path.realpath(self.outdir), self.builder.outdir))
365-
runtime.append(u"--volume=%s:%s:rw" % (os.path.realpath(self.tmpdir), "/tmp"))
375+
runtime.append(u"--volume=%s:%s:rw" % (docker_windows_path_adjust(os.path.realpath(self.outdir)), self.builder.outdir))
376+
runtime.append(u"--volume=%s:%s:rw" % (docker_windows_path_adjust(os.path.realpath(self.tmpdir)), "/tmp"))
366377

367378
self.add_volumes(self.pathmapper, runtime, False)
368379
if self.generatemapper:
369380
self.add_volumes(self.generatemapper, runtime, True)
370381

371-
runtime.append(u"--workdir=%s" % (self.builder.outdir))
382+
runtime.append(u"--workdir=%s" % (docker_windows_path_adjust(self.builder.outdir)))
372383
runtime.append(u"--read-only=true")
373384

374385
if kwargs.get("custom_net", None) is not None:
@@ -379,9 +390,12 @@ def run(self, pull_image=True, rm_container=True,
379390
if self.stdout:
380391
runtime.append("--log-driver=none")
381392

382-
euid = docker_vm_uid() or os.geteuid()
393+
if onWindows(): # windows os dont have getuid or geteuid functions
394+
euid = docker_vm_uid()
395+
else:
396+
euid = docker_vm_uid() or os.geteuid()
383397

384-
if kwargs.get("no_match_user", None) is False:
398+
if kwargs.get("no_match_user", None) is False and euid is not None:
385399
runtime.append(u"--user=%s" % (euid))
386400

387401
if rm_container:
@@ -436,7 +450,7 @@ def _job_popen(
436450

437451
sp = subprocess.Popen(commands,
438452
shell=False,
439-
close_fds=True,
453+
close_fds=not onWindows(),
440454
stdin=stdin,
441455
stdout=stdout,
442456
stderr=stderr,
@@ -478,14 +492,14 @@ def _job_popen(
478492
stderr_path=stderr_path,
479493
stdin_path=stdin_path,
480494
)
481-
with open(os.path.join(job_dir, "job.json"), "w") as f:
495+
with open(os.path.join(job_dir, "job.json"), "wb") as f:
482496
json.dump(job_description, f)
483497
try:
484498
job_script = os.path.join(job_dir, "run_job.bash")
485499
with open(job_script, "wb") as f:
486500
f.write(job_script_contents.encode('utf-8'))
487501
job_run = os.path.join(job_dir, "run_job.py")
488-
with open(job_run, "w") as f:
502+
with open(job_run, "wb") as f:
489503
f.write(PYTHON_RUN_SCRIPT)
490504
sp = subprocess.Popen(
491505
["bash", job_script.encode("utf-8")],

cwltool/load_tool.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ def fetch_document(argsworkflow, # type: Union[Text, Dict[Text, Any]]
4747
workflowobj = None # type: CommentedMap
4848
if isinstance(argsworkflow, string_types):
4949
split = urllib.parse.urlsplit(argsworkflow)
50-
if split.scheme:
50+
# In case of Windows path, urlsplit misjudge Drive letters as scheme, here we are skipping that
51+
if split.scheme and split.scheme in [u'http',u'https',u'file']:
5152
uri = argsworkflow
5253
elif os.path.exists(os.path.abspath(argsworkflow)):
5354
uri = file_uri(str(os.path.abspath(argsworkflow)))

cwltool/main.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
from .software_requirements import DependenciesConfiguration, get_container_from_software_requirements, SOFTWARE_REQUIREMENTS_ENABLED
4040
from .stdfsaccess import StdFsAccess
4141
from .update import ALLUPDATES, UPDATES
42+
from .utils import onWindows
43+
from ruamel.yaml.comments import Comment, CommentedSeq, CommentedMap
4244

4345

4446
_logger = logging.getLogger("cwltool")
@@ -695,6 +697,10 @@ def main(argsl=None, # type: List[str]
695697
argsl = sys.argv[1:]
696698
args = arg_parser().parse_args(argsl)
697699

700+
# If On windows platform, A default Docker Container is Used if not explicitely provided by user
701+
if onWindows() and not args.default_container:
702+
args.default_container = "ubuntu"
703+
698704
# If caller provided custom arguments, it may be not every expected
699705
# option is set, so fill in no-op defaults to avoid crashing when
700706
# dereferencing them in args.

cwltool/pathmapper.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from schema_salad.sourceline import SourceLine
1313
from six.moves import urllib
1414

15+
from .utils import convert_pathsep_to_unix
16+
1517
from .stdfsaccess import StdFsAccess, abspath
1618

1719
_logger = logging.getLogger("cwltool")
@@ -186,7 +188,8 @@ def visitlisting(self, listing, stagedir, basedir, copy=False, staged=False):
186188

187189
def visit(self, obj, stagedir, basedir, copy=False, staged=False):
188190
# type: (Dict[Text, Any], Text, Text, bool, bool) -> None
189-
tgt = os.path.join(stagedir, obj["basename"])
191+
tgt = convert_pathsep_to_unix(
192+
os.path.join(stagedir, obj["basename"]))
190193
if obj["location"] in self._pathmap:
191194
return
192195
if obj["class"] == "Directory":

0 commit comments

Comments
 (0)