Skip to content

Commit f798c7e

Browse files
committed
Adding new command: planemo dependency_script
This is a squashed commit of pull request #310 for issue #303, for converting tool_dependencies.xml install recipes into bash scripts. There is a lot that could be done better, or added, including: - refactor to use a visitor pattern instead of my .to_bash() methods - expand the command line API with options for paths and filenames - setting defaults like download cache via ~/.planemo.yml - complete the action coverage (especially the R/Python/Perl environments) - avoid collisions in the download cache which currently assumes unique filenames However, this is enough to help with automating dependency installation in a continuous integration setup like TravisCI.
1 parent c857676 commit f798c7e

File tree

4 files changed

+596
-5
lines changed

4 files changed

+596
-5
lines changed
+235
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import os
2+
import sys
3+
4+
import click
5+
6+
from xml.etree import ElementTree as ET
7+
8+
from planemo.io import info, error
9+
from planemo.cli import pass_context
10+
from planemo import options
11+
12+
from planemo.shed2tap.base import BasePackage, Dependency
13+
14+
15+
# We're using strict bash mode for dep_install.sh:
16+
preamble_dep_install = """#!/bin/bash
17+
#set -euo pipefail
18+
set -eo pipefail
19+
if [[ ! -d $INSTALL_DIR ]]
20+
then
21+
echo "ERROR: Environment variable INSTALL_DIR not a directory!"
22+
exit 1
23+
fi
24+
# Set full strict mode now, side stepping case $INSTALL_DIR not setup.
25+
set -euo pipefail
26+
export DOWNLOAD_CACHE=`(cd "$DOWNLOAD_CACHE"; pwd)`
27+
if [[ ! -d $DOWNLOAD_CACHE ]]
28+
then
29+
mkdir -p $DOWNLOAD_CACHE
30+
fi
31+
echo "Using $DOWNLOAD_CACHE for cached downloads."
32+
export INSTALL_DIR=`(cd "$INSTALL_DIR"; pwd)`
33+
echo "Using $INSTALL_DIR for the installed files."
34+
# Create a randomly named temp folder for working in
35+
dep_install_tmp=${TMPDIR-/tmp}/dep_install.$RANDOM.$RANDOM.$RANDOM.$$
36+
(umask 077 && mkdir $dep_install_tmp) || exit 1
37+
"""
38+
39+
final_dep_install = """echo "Cleaning up..."
40+
rm -rf $dep_install_tmp
41+
echo "======================"
42+
echo "Installation complete."
43+
echo "======================"
44+
"""
45+
46+
# Expect user to "source env.sh" so don't set strict mode,
47+
# and don't use the exit command!
48+
preamble_env_sh = """#!/bin/bash
49+
if [[ ! -d $INSTALL_DIR ]]
50+
then
51+
echo "ERROR: Environment variable INSTALL_DIR not a directory!"
52+
fi
53+
export INSTALL_DIR=${INSTALL_DIR:-$PWD}
54+
export INSTALL_DIR=`(cd "$INSTALL_DIR"; pwd)`
55+
"""
56+
57+
58+
def find_tool_dependencis_xml(path, recursive):
59+
"""Iterator function, quick & dirty tree walking."""
60+
if os.path.isfile(path):
61+
if os.path.basename(path) == "tool_dependencies.xml":
62+
yield path
63+
elif os.path.isdir(path):
64+
p = os.path.join(path, "tool_dependencies.xml")
65+
if os.path.isfile(p):
66+
yield p
67+
if recursive:
68+
for f in sorted(os.listdir(path)):
69+
p = os.path.join(path, f)
70+
if os.path.isdir(p):
71+
# TODO: yield from
72+
for x in find_tool_dependencis_xml(p, recursive):
73+
yield x
74+
75+
76+
def convert_tool_dep(dependencies_file):
77+
"""Parse a tool_dependencies.xml into install.sh and env.sh commands.
78+
79+
Returns two lists of strings, commands to add to install.sh and
80+
env.sh respectively.
81+
"""
82+
install_cmds = []
83+
env_cmds = []
84+
85+
root = ET.parse(dependencies_file).getroot()
86+
package_els = root.findall("package")
87+
88+
packages = []
89+
dependencies = []
90+
for package_el in package_els:
91+
install_els = package_el.findall("install")
92+
assert len(install_els) in (0, 1)
93+
if len(install_els) == 0:
94+
repository_el = package_el.find("repository")
95+
assert repository_el is not None, "no repository in %s" % repository_el
96+
dependencies.append(Dependency(None, package_el, repository_el))
97+
else:
98+
install_el = install_els[0]
99+
packages.append(BasePackage(None, package_el, install_el, readme=None))
100+
101+
if not packages:
102+
info("No packages in %s" % dependencies_file)
103+
return [], []
104+
105+
assert len(packages) == 1, packages
106+
package = packages[0]
107+
name = package_el.attrib["name"]
108+
version = package_el.attrib["version"]
109+
110+
# TODO - Set $INSTALL_DIR in the script
111+
# os.environ["INSTALL_DIR"] = os.path.abspath(os.curdir)
112+
for action in package.all_actions:
113+
inst, env = action.to_bash()
114+
install_cmds.extend(inst)
115+
env_cmds.extend(env)
116+
117+
if install_cmds:
118+
install_cmds.insert(0, 'cd $dep_install_tmp')
119+
install_cmds.insert(0, 'specifc_action_done=0')
120+
install_cmds.insert(0, 'echo "%s"' % ('=' * 60))
121+
install_cmds.insert(0, 'echo "Installing %s version %s"' % (name, version))
122+
install_cmds.insert(0, 'echo "%s"' % ('=' * 60))
123+
if env_cmds:
124+
env_cmds.insert(0, 'specifc_action_done=0')
125+
env_cmds.insert(0, '#' + '=' * 60)
126+
env_cmds.insert(0, 'echo "Setting environment variables for %s version %s"' % (name, version))
127+
env_cmds.insert(0, '#' + '=' * 60)
128+
# TODO - define $INSTALL_DIR here?
129+
130+
return install_cmds, env_cmds
131+
132+
133+
def process_tool_dependencies_xml(tool_dep, install_handle, env_sh_handle):
134+
"""Writes to handles, returns success as a boolean."""
135+
if not os.path.isfile(tool_dep):
136+
error('Missing file %s' % tool_dep)
137+
return False
138+
if not os.stat(tool_dep).st_size:
139+
error('Empty file %s' % tool_dep)
140+
return False
141+
try:
142+
install, env = convert_tool_dep(tool_dep)
143+
except Exception as err:
144+
# TODO - pass in ctx for logging?
145+
error('Error processing %s - %s' %
146+
(click.format_filename(tool_dep), err))
147+
if not isinstance(err, (NotImplementedError, RuntimeError)):
148+
# This is an unexpected error, traceback is useful
149+
import traceback
150+
error(traceback.format_exc() + "\n")
151+
return False
152+
# Worked...
153+
for cmd in install:
154+
install_handle.write(cmd + "\n")
155+
for cmd in env:
156+
env_sh_handle.write(cmd + "\n")
157+
return True
158+
159+
160+
@click.command('dependency_script')
161+
@options.shed_realization_options()
162+
@options.dependencies_script_options()
163+
@pass_context
164+
def cli(ctx, paths, recursive=False, fail_fast=True, download_cache=None):
165+
"""Prepare a bash shell script to install tool requirements (**Experimental**)
166+
167+
An experimental approach parsing tool_dependencies.xml files into
168+
bash shell scripts, intended initially for use within Continuous
169+
Integration testing setups like TravisCI.
170+
171+
Parses the specified ``tool_dependencies.xml`` files, and converts them into
172+
an installation bash script (default ``dep_install.sh``), and a shell script
173+
(default ``env.sh``) defining any new/edited environment variables.
174+
175+
These are intended to be used via ``bash dep_install.sh`` (once), and as
176+
``source env.sh`` prior to running any of the dependencies to set the
177+
environment variable within the current shell session.
178+
179+
Both ``dep_install.sh`` and ``env.sh`` require ``$INSTALL_DIR`` be defined
180+
before running them, set to an existing directory with write permissions.
181+
Beware than if run on multiple tools, they can over-write each other (for
182+
example if you have packages for different versions of the same tool). In
183+
this case make separate calls to ``planemo dependency_script`` and call
184+
the scripts with different installation directories.
185+
186+
This command will download (and cache) any URLs specified via Galaxy
187+
download actions. This is in order to decompress them and determine the
188+
relevant sub-folder to change into as per the Tool Shed install mechanism,
189+
so that this can be recorded as a ``cd`` comand in the bash script.
190+
191+
The download cache used by ``planemo dependency_script`` and the resulting
192+
output script ``dep_install.sh`` defaults to ``./download_cache`` (under
193+
the current working directory), and can be set with ``$DOWNLOAD_CACHE``.
194+
195+
This is experimental, and is initially intended for use within continuous
196+
integration testing setups like TravisCI to both verify the dependency
197+
installation receipe works, and to use this to run functional tests.
198+
"""
199+
# TODO: Command line API for bash output filanames & install dir, cache.
200+
if download_cache:
201+
assert os.path.isdir(download_cache), download_cache
202+
# Effectively using this as a global variable, refactor this
203+
# once using a visitor pattern instead of action.to_bash()
204+
os.environ["DOWNLOAD_CACHE"] = os.path.abspath(download_cache)
205+
print("Using $DOWNLOAD_CACHE=%r" % os.environ["DOWNLOAD_CACHE"])
206+
failed = False
207+
with open("env.sh", "w") as env_sh_handle:
208+
with open("dep_install.sh", "w") as install_handle:
209+
install_handle.write(preamble_dep_install)
210+
env_sh_handle.write(preamble_env_sh)
211+
for path in paths:
212+
# ctx.log("Checking: %r" % path)
213+
if failed and fail_fast:
214+
break
215+
for tool_dep in find_tool_dependencis_xml(path, recursive):
216+
passed = process_tool_dependencies_xml(tool_dep,
217+
install_handle,
218+
env_sh_handle)
219+
if passed:
220+
info('Processed %s' % tool_dep)
221+
else:
222+
failed = True
223+
if fail_fast:
224+
for line in [
225+
'#' + '*' * 60,
226+
'echo "WARNING: Skipping %s"' % tool_dep,
227+
'#' + '*' * 60]:
228+
install_handle.write(line + "\n")
229+
break
230+
# error("%s failed" % tool_dep)
231+
install_handle.write(final_dep_install)
232+
ctx.log("The End")
233+
if failed:
234+
error('Error processing one or more tool_dependencies.xml files.')
235+
sys.exit(1)

planemo/options.py

+12
Original file line numberDiff line numberDiff line change
@@ -620,3 +620,15 @@ def _compose(*functions):
620620
def compose2(f, g):
621621
return lambda x: f(g(x))
622622
return functools.reduce(compose2, functions)
623+
624+
625+
def dependencies_script_options():
626+
return _compose(
627+
click.option(
628+
"--download_cache",
629+
type=click.Path(file_okay=False, resolve_path=True),
630+
callback=get_default_callback(None),
631+
help=("Directory to cache downloaded files, default is $DOWNLOAD_CACHE"),
632+
default=None,
633+
),
634+
)

0 commit comments

Comments
 (0)