swc-windows-installer.py: Look for an R bin directory
[swc-setup-windows-installer.git] / swc-windows-installer.py
1 #!/usr/bin/env python
2
3 """Software Carpentry Windows Installer
4
5 Helps mimic a *nix environment on Windows with as little work as possible.
6
7 The script:
8 * Installs nano and makes it accessible from msysgit
9 * Installs sqlite3 and makes it accessible from msysGit
10 * Creates ~/nano.rc with links to syntax highlighting configs
11 * Provides standard nosetests behavior for msysgit
12 * Add R's bin directory to the path (if we can find it)
13
14 To use:
15
16 1. Install Python, IPython, and Nose.  An easy way to do this is with
17    the Anaconda CE Python distribution
18    http://continuum.io/anacondace.html
19 2. Install msysgit
20    http://code.google.com/p/msysgit/downloads/list
21 3. Run swc_windows_installer.py
22    You should be able to simply double click the file in Windows
23
24 """
25
26 import glob
27 import hashlib
28 try:  # Python 3
29     from io import BytesIO as _BytesIO
30 except ImportError:  # Python 2
31     from StringIO import StringIO as _BytesIO
32 import os
33 import re
34 import sys
35 import tarfile
36 try:  # Python 3
37     from urllib.request import urlopen as _urlopen
38 except ImportError:  # Python 2
39     from urllib2 import urlopen as _urlopen
40 import zipfile
41
42
43 if sys.version_info >= (3, 0):  # Python 3
44     open3 = open
45 else:
46     def open3(file, mode='r', newline=None):
47         if newline:
48             if newline != '\n':
49                 raise NotImplementedError(newline)
50             f = open(file, mode + 'b')
51         else:
52             f = open(file, mode)
53         return f
54
55
56 def download(url, sha1):
57     """Download a file and verify it's hash"""
58     r = _urlopen(url)
59     byte_content = r.read()
60     download_sha1 = hashlib.sha1(byte_content).hexdigest()
61     if download_sha1 != sha1:
62         raise ValueError(
63             'downloaded {!r} has the wrong SHA1 hash: {} != {}'.format(
64                 url, download_sha1, sha1))
65     return byte_content
66
67
68 def splitall(path):
69     """Split a path into a list of components
70
71     >>> splitall('nano-2.2.6/doc/Makefile.am')
72     ['nano-2.2.6', 'doc', 'Makefile.am']
73     """
74     parts = []
75     while True:
76         head, tail = os.path.split(path)
77         if tail:
78             parts.insert(0, tail)
79         elif head:
80             parts.insert(0, head)
81             break
82         else:
83             break
84         path = head
85     return parts
86
87
88 def transform(tarinfo, strip_components=0):
89     """Transform TarInfo objects for extraction"""
90     path_components = splitall(tarinfo.name)
91     try:
92         tarinfo.name = os.path.join(*path_components[strip_components:])
93     except TypeError:
94         if len(path_components) <= strip_components:
95             return None
96         raise
97     return tarinfo
98
99
100 def tar_install(url, sha1, install_directory, compression='*',
101                 strip_components=0):
102     """Download and install a tar bundle"""
103     if not os.path.isdir(install_directory):
104         tar_bytes = download(url=url, sha1=sha1)
105         tar_io = _BytesIO(tar_bytes)
106         filename = os.path.basename(url)
107         mode = 'r:{}'.format(compression)
108         tar_file = tarfile.open(filename, mode, tar_io)
109         os.makedirs(install_directory)
110         members = [
111             transform(tarinfo=tarinfo, strip_components=strip_components)
112             for tarinfo in tar_file]
113         tar_file.extractall(
114             path=install_directory,
115             members=[m for m in members if m is not None])
116
117
118 def zip_install(url, sha1, install_directory):
119     """Download and install a zipped bundle"""
120     if not os.path.isdir(install_directory):
121         zip_bytes = download(url=url, sha1=sha1)
122         zip_io = _BytesIO(zip_bytes)
123         zip_file = zipfile.ZipFile(zip_io)
124         os.makedirs(install_directory)
125         zip_file.extractall(install_directory)
126
127
128 def install_nano(install_directory):
129     """Download and install the nano text editor"""
130     zip_install(
131         url='http://www.nano-editor.org/dist/v2.2/NT/nano-2.2.6.zip',
132         sha1='f5348208158157060de0a4df339401f36250fe5b',
133         install_directory=install_directory)
134
135
136 def install_nanorc(install_directory):
137     """Download and install nano syntax highlighting"""
138     tar_install(
139         url='http://www.nano-editor.org/dist/v2.2/nano-2.2.6.tar.gz',
140         sha1='f2a628394f8dda1b9f28c7e7b89ccb9a6dbd302a',
141         install_directory=install_directory,
142         strip_components=1)
143     home = os.path.expanduser('~')
144     nanorc = os.path.join(home, 'nano.rc')
145     if not os.path.isfile(nanorc):
146         syntax_dir = os.path.join(install_directory, 'doc', 'syntax')
147         with open3(nanorc, 'w', newline='\n') as f:
148             for filename in os.listdir(syntax_dir):
149                 if filename.endswith('.nanorc'):
150                     path = os.path.join(syntax_dir, filename)
151                     rel_path = os.path.relpath(path, home)
152                     include_path = make_posix_path(os.path.join('~', rel_path))
153                     f.write('include {}\n'.format(include_path))
154
155
156 def install_sqlite(install_directory):
157     """Download and install the sqlite3 shell"""
158     zip_install(
159         url='https://sqlite.org/2014/sqlite-shell-win32-x86-3080403.zip',
160         sha1='1a8ab0ca9f4c51afeffeb49bd301e1d7f64741bb',
161         install_directory=install_directory)
162
163
164 def create_nosetests_entry_point(python_scripts_directory):
165     """Creates a terminal-based nosetests entry point for msysgit"""
166     contents = '\n'.join([
167             '#!/usr/bin/env/ python',
168             'import sys',
169             'import nose',
170             "if __name__ == '__main__':",
171             '    sys.exit(nose.core.main())',
172             '',
173             ])
174     if not os.path.isdir(python_scripts_directory):
175         os.makedirs(python_scripts_directory)
176     with open(os.path.join(python_scripts_directory, 'nosetests'), 'w') as f:
177         f.write(contents)
178
179
180 def get_r_bin_directory():
181     """Locate the R bin directory (if R is installed
182     """
183     pf = _os.environ.get('ProgramFiles', r'c:\ProgramFiles')
184     bin_glob = os.path.join(pf, 'R', 'R-[0-9]*.[0-9]*.[0-9]*', 'bin')
185     version_re = re.compile('^R-(\d+)[.](\d+)[.](\d+)$')
186     paths = {}
187     for path in glob.glob(bin_glob):
188         version_dir = os.path.basename(os.path.dirname(path))
189         version_match = version_re.match(version_dir)
190         if version_match:
191             paths[version_match.groups()] = path
192     version = sorted(paths.keys())[-1]
193     return paths[version]
194
195
196 def update_bash_profile(extra_paths=()):
197     """Create or append to a .bash_profile for Software Carpentry
198
199     Adds nano to the path, sets the default editor to nano, and adds
200     additional paths for other executables.
201     """
202     lines = [
203         '',
204         '# Add paths for Software-Carpentry-installed scripts and executables',
205         'export PATH=\"$PATH:{}\"'.format(':'.join(
206             make_posix_path(path) for path in extra_paths),),
207         '',
208         '# Make nano the default editor',
209         'export EDITOR=nano',
210         '',
211         ]
212     config_path = os.path.join(os.path.expanduser('~'), '.bash_profile')
213     with open(config_path, 'a') as f:
214         f.write('\n'.join(lines))
215
216
217 def make_posix_path(windows_path):
218     """Convert a Windows path to a posix path"""
219     for regex, sub in [
220             (re.compile(r'\\'), '/'),
221             (re.compile('^[Cc]:'), '/c'),
222             ]:
223         windows_path = regex.sub(sub, windows_path)
224     return windows_path
225
226
227 def main():
228     swc_dir = os.path.join(os.path.expanduser('~'), '.swc')
229     bin_dir = os.path.join(swc_dir, 'bin')
230     nano_dir = os.path.join(swc_dir, 'lib', 'nano')
231     nanorc_dir = os.path.join(swc_dir, 'share', 'nanorc')
232     sqlite_dir = os.path.join(swc_dir, 'lib', 'sqlite')
233     create_nosetests_entry_point(python_scripts_directory=bin_dir)
234     install_nano(install_directory=nano_dir)
235     install_nanorc(install_directory=nanorc_dir)
236     install_sqlite(install_directory=sqlite_dir)
237     paths = [nano_dir, sqlite_dir, bin_dir]
238     r_dir = get_r_bin_directory()
239     if r_dir:
240         paths.append(r_dir)
241     update_bash_profile(extra_paths=paths)
242
243
244 if __name__ == '__main__':
245     print("Preparing your Software Carpentry awesomeness!")
246     main()